Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
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 @@ -46979,7 +46979,6 @@
"xpack.synthetics.addEditMonitor.scriptEditor.helpText": "Führt synthetische Testskripte aus, die inline definiert sind.",
"xpack.synthetics.addEditMonitor.scriptEditor.label": "Skript-Editor",
"xpack.synthetics.addEditMonitor.scriptEditor.placeholder": "// Fügen Sie hier Ihr Playwright-Skript ein...",
"xpack.synthetics.addMonitor.forbidden": "Sie verfügen nicht über ausreichende Berechtigungen, um Monitore in allen erforderlichen Bereichen zu aktualisieren.",
"xpack.synthetics.aiAssistant.starterPrompts.explainNoData.prompt": "Warum sehe ich keine Überwachungen?",
"xpack.synthetics.aiAssistant.starterPrompts.explainNoData.title": "Erklären",
"xpack.synthetics.alertDropdown.noPermissions": "Sie haben nicht die erforderlichen Berechtigungen, um diese Aktion auszuführen.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47021,7 +47021,6 @@
"xpack.synthetics.addEditMonitor.scriptEditor.helpText": "Exécute des scripts de tests synthétiques définis en ligne.",
"xpack.synthetics.addEditMonitor.scriptEditor.label": "Éditeur de script",
"xpack.synthetics.addEditMonitor.scriptEditor.placeholder": "// Collez votre script Playwright ici...",
"xpack.synthetics.addMonitor.forbidden": "Vous n’avez pas suffisamment d’autorisations pour mettre à jour les moniteurs dans tous les espaces requis.",
"xpack.synthetics.aiAssistant.starterPrompts.explainNoData.prompt": "Pourquoi n'y a-t-il aucun moniteur ?",
"xpack.synthetics.aiAssistant.starterPrompts.explainNoData.title": "Expliquer",
"xpack.synthetics.alertDropdown.noPermissions": "Vous ne disposez pas d'autorisations suffisantes pour effectuer cette action.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47172,7 +47172,6 @@
"xpack.synthetics.addEditMonitor.scriptEditor.helpText": "インラインで定義されたSyntheticテストスクリプトを実行します。",
"xpack.synthetics.addEditMonitor.scriptEditor.label": "スクリプトエディター",
"xpack.synthetics.addEditMonitor.scriptEditor.placeholder": "// ここにPlaywrightスクリプトを貼り付け...",
"xpack.synthetics.addMonitor.forbidden": "必要なすべてのスペースでモニターを更新する十分な権限がありません。",
"xpack.synthetics.aiAssistant.starterPrompts.explainNoData.prompt": "モニターが表示されていない理由",
"xpack.synthetics.aiAssistant.starterPrompts.explainNoData.title": "説明",
"xpack.synthetics.alertDropdown.noPermissions": "このアクションを実行する十分な権限がありません。",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47173,7 +47173,6 @@
"xpack.synthetics.addEditMonitor.scriptEditor.helpText": "运行内联定义的 Synthetics 测试脚本。",
"xpack.synthetics.addEditMonitor.scriptEditor.label": "脚本编辑器",
"xpack.synthetics.addEditMonitor.scriptEditor.placeholder": "// 在此处粘贴 Playwright 脚本......",
"xpack.synthetics.addMonitor.forbidden": "您没有足够的权限在全部所需工作区中更新监测。",
"xpack.synthetics.aiAssistant.starterPrompts.explainNoData.prompt": "为什么我看不到任何监测?",
"xpack.synthetics.aiAssistant.starterPrompts.explainNoData.title": "解释",
"xpack.synthetics.alertDropdown.noPermissions": "您的权限不足,无法执行此操作。",
Expand Down
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
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 @@ -48,6 +48,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,16 @@ 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 type { MonitorFields } 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 +134,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 as MonitorFields,
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 @@ -171,7 +171,7 @@ export class AddEditMonitorAPI {
monitorPayload: CreateMonitorPayLoad,
prevLocations?: MonitorFields['locations']
) {
const { savedObjectsClient, syntheticsMonitorClient, request } = this.routeContext;
const { syntheticsMonitorClient, request } = this.routeContext;
const internal = Boolean((request.query as { internal?: boolean })?.internal);
const {
locations,
Expand All @@ -195,11 +195,34 @@ export class AddEditMonitorAPI {

if (!locations && !privateLocations && prevLocations) {
locationsVal = prevLocations;

const prevPrivateLocations = prevLocations.filter((loc) => !loc.isServiceManaged);
if (prevPrivateLocations.length > 0) {
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 {
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 @@ -252,8 +282,7 @@ export const syncEditedMonitor = async ({
routeContext: RouteContext;
spaceId: string;
}) => {
const { server, savedObjectsClient, syntheticsMonitorClient, monitorConfigRepository } =
routeContext;
const { server, syntheticsMonitorClient, monitorConfigRepository } = routeContext;

const monitorId = decryptedPreviousMonitor.id;
const monitorPrivateLocations = normalizedMonitor[ConfigKey.LOCATIONS].filter(
Expand All @@ -276,7 +305,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