Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e6ab82e
validate private location spaces for multi-space monitors
benakansara Apr 1, 2026
3eefe79
fix validation bypass when editing monitor spaces without changing lo…
benakansara Apr 1, 2026
b503818
Merge branch 'main' into feat/multi-space-monitors-and-private-locations
benakansara Apr 1, 2026
c139955
fix types
benakansara Apr 1, 2026
fb46fda
Merge branch 'main' into feat/multi-space-monitors-and-private-locations
benakansara Apr 8, 2026
458cd06
Merge branch 'main' into feat/multi-space-monitors-and-private-locations
shahzad31 Apr 13, 2026
9cf7606
Merge branch 'main' into feat/multi-space-monitors-and-private-locations
benakansara Apr 27, 2026
9f01d59
remove unused translation key
benakansara Apr 27, 2026
a15a34c
share private location to spaces
benakansara Apr 27, 2026
ead32a2
update tests
benakansara Apr 27, 2026
85dbfd7
Changes from node scripts/lint_ts_projects --fix
kibanamachine Apr 27, 2026
efedd66
Changes from node scripts/regenerate_moon_projects.js --update
kibanamachine Apr 27, 2026
c63fd76
update tests
benakansara Apr 27, 2026
6257c10
update tests
benakansara Apr 27, 2026
fe0f41c
Merge branch 'main' into feat/multi-space-monitors-and-private-locations
benakansara Apr 27, 2026
de80e6e
Merge branch 'main' into feat/multi-space-monitors-and-private-locations
benakansara Apr 28, 2026
331ac59
use correct so type
benakansara Apr 30, 2026
2baeb18
use correct so type when editing private location
benakansara Apr 30, 2026
94d542e
Merge branch 'main' into feat/multi-space-monitors-and-private-locations
benakansara Apr 30, 2026
125c036
Merge branch 'main' into feat/multi-space-monitors-and-private-locations
benakansara May 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
FieldPassword,
Checkbox,
ComboBox,
LocationsComboBox,
Select,
Switch,
Source,
Expand Down Expand Up @@ -428,7 +429,7 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
fieldKey: ConfigKey.LOCATIONS,
required: true,
controlled: true,
component: ComboBox,
component: LocationsComboBox,
label: i18n.translate('xpack.synthetics.monitorConfig.locations.label', {
defaultMessage: 'Locations',
}),
Expand Down Expand Up @@ -1345,7 +1346,7 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
id="xpack.synthetics.monitorConfig.throttlingDisabled.label"
defaultMessage="Connection profile ( {icon} Important information about throttling: {link})"
values={{
icon: <EuiIcon type="alert" color="warning" size="s" />,
icon: <EuiIcon type="alert" color="warning" size="s" aria-hidden={true} />,
link: (
<EuiLink
data-test-subj="syntheticsFIELDNoticeLink"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import type { Ref } from 'react';
import React from 'react';
import { omit } from 'lodash';
import { i18n } from '@kbn/i18n';
import type { ControllerRenderProps } from 'react-hook-form';
import type {
EuiFieldTextProps,
Expand All @@ -30,6 +31,8 @@ import {
EuiButtonGroup,
EuiComboBox,
EuiTextArea,
EuiText,
EuiSpacer,
} from '@elastic/eui';
import type { MonitorSpacesProps } from '../fields/monitor_spaces';
import { MonitorSpaces } from '../fields/monitor_spaces';
Expand Down Expand Up @@ -119,6 +122,36 @@ export const ComboBox = React.forwardRef<unknown, EuiComboBoxProps<unknown>>((pr
<EuiComboBox {...omit(props, ['isServiceManaged'])} />
));

export const LocationsComboBox = React.forwardRef<unknown, EuiComboBoxProps<unknown>>(
(props, _ref) => {
const { selectedOptions, options } = props;
const optionIds = new Set((options ?? []).map((o) => o.id));
const unavailableLocations = (selectedOptions ?? []).filter(
(sel) =>
!(sel as unknown as { isServiceManaged?: boolean }).isServiceManaged &&
!optionIds.has(sel.id)
);

return (
<>
<EuiComboBox {...omit(props, ['isServiceManaged'])} />
{unavailableLocations.length > 0 && (
<>
<EuiSpacer size="xs" />
<EuiText size="xs" color="danger">
{i18n.translate('xpack.synthetics.monitorConfig.locations.notInSpaceWarning', {
defaultMessage:
'{count, plural, one {# private location is} other {# private locations are}} not available in all spaces this monitor is shared to. Share the {count, plural, one {location} other {locations}} to all monitor spaces, or remove {count, plural, one {it} other {them}} from the monitor.',
values: { count: unavailableLocations.length },
})}
</EuiText>
</>
)}
</>
);
}
);

export const JSONEditor = React.forwardRef<unknown, DefaultCodeEditorProps>((props, _ref) => (
<DefaultJSONEditor {...props} />
));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,21 @@ export const getServerMock = () => {
},
},
encryptedSavedObjects: mockEncryptedSO(),
coreStart: {
savedObjects: {
createInternalRepository: jest.fn().mockReturnValue({
createPointInTimeFinder: jest.fn().mockImplementation(() => ({
close: jest.fn(async () => {}),
find: jest.fn().mockReturnValue({
async *[Symbol.asyncIterator]() {
yield { saved_objects: [] };
},
}),
})),
get: jest.fn(),
}),
},
},
} as unknown as SyntheticsServerSetup;

return serverMock;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,15 @@ import {
import type { CreateMonitorPayLoad } from './add_monitor/add_monitor_api';
import { AddEditMonitorAPI } from './add_monitor/add_monitor_api';
import type { SyntheticsRestApiRouteFactory } from '../types';
import { ConfigKey } from '../../../common/runtime_types';
import { SYNTHETICS_API_URLS } from '../../../common/constants';
import { normalizeAPIConfig, validateMonitor } from './monitor_validation';
import { mapSavedObjectToMonitor } from './formatters/saved_object_to_monitor';
import { getBrowserTimeoutWarningForMonitor } from './monitor_warnings';
import {
assertCanUpdateMonitorInAllSpaces,
validateMonitorPrivateLocationSpaces,
} from './monitor_locations_utils';

export const addSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
method: 'POST',
Expand Down Expand Up @@ -128,6 +133,29 @@ export const addSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => ({
});
}

const monitorSpaces = normalizedMonitor[ConfigKey.KIBANA_SPACES] ?? [];
if (monitorSpaces.length > 0) {
const spaceAuthError = await assertCanUpdateMonitorInAllSpaces(routeContext, monitorSpaces);
if (spaceAuthError) {
return spaceAuthError;
}
}

if (addMonitorAPI.allPrivateLocations && addMonitorAPI.allPrivateLocations.length > 0) {
const plSpaceError = validateMonitorPrivateLocationSpaces(
normalizedMonitor,
addMonitorAPI.allPrivateLocations
);
if (plSpaceError) {
return response.badRequest({
body: {
message: plSpaceError.message,
attributes: plSpaceError.attributes,
},
});
}
}

const { errors, newMonitor } = await addMonitorAPI.syncNewMonitor({
id,
normalizedMonitor,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import { DefaultRuleService } from '../../default_alerts/default_alert_service';
import type { RouteContext } from '../../types';
import { formatTelemetryEvent, sendTelemetryEvents } from '../../telemetry/monitor_upgrade_sender';
import { formatKibanaNamespace } from '../../../../common/formatters';
import { getPrivateLocations } from '../../../synthetics_service/get_private_locations';
import { getPrivateLocationsForNamespaces } from '../../../synthetics_service/get_private_locations';

export type CreateMonitorPayLoad = MonitorFields & {
url?: string;
Expand Down Expand Up @@ -199,7 +199,16 @@ export class AddEditMonitorAPI {
const monitorLocations = parseMonitorLocations(monitorPayload, prevLocations, internal);

if (monitorLocations.privateLocations.length > 0) {
this.allPrivateLocations = await getPrivateLocations(savedObjectsClient);
const monitorSpaces = monitor[ConfigKey.KIBANA_SPACES] ?? [];
const namespacesForLookup = [
...new Set([this.routeContext.spaceId, ...monitorSpaces]),
].filter(Boolean);
const internalClient =
this.routeContext.server.coreStart.savedObjects.createInternalRepository();
this.allPrivateLocations = await getPrivateLocationsForNamespaces(
internalClient,
namespacesForLookup
);
} else {
this.allPrivateLocations = [];
}
Expand Down
Comment thread
benakansara marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ import {
import type { CreateMonitorPayLoad } from './add_monitor/add_monitor_api';
import { AddEditMonitorAPI } from './add_monitor/add_monitor_api';
import { ELASTIC_MANAGED_LOCATIONS_DISABLED } from './project_monitor/add_monitor_project';
import { getPrivateLocations } from '../../synthetics_service/get_private_locations';
import { getPrivateLocationsForNamespaces } from '../../synthetics_service/get_private_locations';
import { mergeSourceMonitor } from './formatters/saved_object_to_monitor';
import {
assertCanUpdateMonitorInAllSpaces,
validateMonitorPrivateLocationSpaces,
} from './monitor_locations_utils';
import type { RouteContext, SyntheticsRestApiRouteFactory } from '../types';
import type {
MonitorFields,
Expand Down Expand Up @@ -141,6 +145,32 @@ export const editSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () => (
});
}

const editedMonitorSpaces = (editedMonitor as MonitorFields)[ConfigKey.KIBANA_SPACES] ?? [];
if (editedMonitorSpaces.length > 0) {
const spaceAuthError = await assertCanUpdateMonitorInAllSpaces(
routeContext,
editedMonitorSpaces
);
if (spaceAuthError) {
return spaceAuthError;
}
}

if (editMonitorAPI.allPrivateLocations && editMonitorAPI.allPrivateLocations.length > 0) {
const plSpaceError = validateMonitorPrivateLocationSpaces(
editedMonitor as MonitorFields,
editMonitorAPI.allPrivateLocations
);
if (plSpaceError) {
return response.badRequest({
body: {
message: plSpaceError.message,
attributes: plSpaceError.attributes,
},
});
}
}

const monitorWithRevision = {
...validationResult.decodedMonitor,
/* reset config hash to empty string. Ensures that the synthetics agent is able
Expand Down Expand Up @@ -276,7 +306,13 @@ export const syncEditedMonitor = async ({
};
const formattedMonitor = formatSecrets(monitorWithId);

const allPrivateLocations = await getPrivateLocations(savedObjectsClient);
const monitorSpaces = (monitorWithId as MonitorFields)[ConfigKey.KIBANA_SPACES] ?? [];
const namespacesForLookup = [...new Set([spaceId, ...monitorSpaces])].filter(Boolean);
const internalClient = server.coreStart.savedObjects.createInternalRepository();
const allPrivateLocations = await getPrivateLocationsForNamespaces(
internalClient,
namespacesForLookup
);

const [editedMonitorSavedObject, { publicSyncErrors, failedPolicyUpdates }] = await Promise.all(
[
Expand Down
Loading
Loading