diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/components/permissions.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/components/permissions.tsx index 65b9732e217e9..4bc09bc9884d0 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/components/permissions.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/components/permissions.tsx @@ -33,15 +33,21 @@ export const FleetPermissionsCallout = () => { export const NoPermissionsTooltip = ({ canEditSynthetics = true, canUsePublicLocations = true, + canManagePrivateLocations = true, children, }: { canEditSynthetics?: boolean; canUsePublicLocations?: boolean; + canManagePrivateLocations?: boolean; children: ReactNode; }) => { const { isServiceAllowed } = useEnablement(); - const disabledMessage = getRestrictionReasonLabel(canEditSynthetics, canUsePublicLocations); + const disabledMessage = getRestrictionReasonLabel( + canEditSynthetics, + canUsePublicLocations, + canManagePrivateLocations + ); if (!isServiceAllowed) { return ( @@ -64,13 +70,18 @@ export const NoPermissionsTooltip = ({ function getRestrictionReasonLabel( canEditSynthetics = true, - canUsePublicLocations = true + canUsePublicLocations = true, + canManagePrivateLocations = true ): string | undefined { const message = !canEditSynthetics ? CANNOT_PERFORM_ACTION_SYNTHETICS : undefined; if (message) { return message; } + if (!canManagePrivateLocations) { + return NEED_PERMISSIONS_PRIVATE_LOCATIONS; + } + return !canUsePublicLocations ? CANNOT_PERFORM_ACTION_PUBLIC_LOCATIONS : undefined; } diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/settings/private_locations/locations_table.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/settings/private_locations/locations_table.tsx index 8f8fe31a638cf..ac6b471c9046f 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/settings/private_locations/locations_table.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/settings/private_locations/locations_table.tsx @@ -52,7 +52,7 @@ export const PrivateLocationsTable = ({ const { locationMonitors, loading } = useLocationMonitors(); - const { canSave } = useSyntheticsSettingsContext(); + const { canSave, canManagePrivateLocations } = useSyntheticsSettingsContext(); const tagsList = privateLocations.reduce((acc, item) => { const tags = item.tags || []; @@ -128,13 +128,16 @@ export const PrivateLocationsTable = ({ const renderToolRight = () => { return [ - + setIsAddingNew(true)} iconType="plusInCircle" > diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/settings/private_locations/manage_private_locations.test.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/settings/private_locations/manage_private_locations.test.tsx index 24c32569f1f2a..5cabd6cf13742 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/settings/private_locations/manage_private_locations.test.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/settings/private_locations/manage_private_locations.test.tsx @@ -119,6 +119,7 @@ describe('', () => { const privateLocationName = 'Test private location'; jest.spyOn(settingsHooks, 'useSyntheticsSettingsContext').mockReturnValue({ canSave, + canManagePrivateLocations: true, } as SyntheticsSettingsContextValues); jest.spyOn(locationHooks, 'usePrivateLocationsAPI').mockReturnValue({ diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/contexts/synthetics_settings_context.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/contexts/synthetics_settings_context.tsx index d380d95c90aa4..518920ee0fd52 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/contexts/synthetics_settings_context.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/contexts/synthetics_settings_context.tsx @@ -45,6 +45,7 @@ export interface SyntheticsAppProps { export interface SyntheticsSettingsContextValues { canSave: boolean; + canManagePrivateLocations: boolean; basePath: string; dateRangeStart: string; dateRangeEnd: string; @@ -74,6 +75,7 @@ const defaultContext: SyntheticsSettingsContextValues = { isLogsAvailable: true, isDev: false, canSave: false, + canManagePrivateLocations: false, }; export const SyntheticsSettingsContext = createContext(defaultContext); @@ -96,6 +98,8 @@ export const SyntheticsSettingsContextProvider: React.FC { return { @@ -109,6 +113,7 @@ export const SyntheticsSettingsContextProvider: React.FC; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_breadcrumbs.test.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_breadcrumbs.test.tsx index c97cefc194069..687c2e833151d 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_breadcrumbs.test.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/hooks/use_breadcrumbs.test.tsx @@ -63,6 +63,7 @@ describe('useBreadcrumbs', () => { setBreadcrumbs: core.chrome.setBreadcrumbs, isInfraAvailable: false, isLogsAvailable: false, + canManagePrivateLocations: false, }} > diff --git a/x-pack/plugins/observability_solution/synthetics/server/feature.ts b/x-pack/plugins/observability_solution/synthetics/server/feature.ts index 5a4c4d508853b..900e655b0cc0d 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/feature.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/feature.ts @@ -32,6 +32,8 @@ const UPTIME_RULE_TYPES = [ 'xpack.uptime.alerts.durationAnomaly', ]; +export const PRIVATE_LOCATION_WRITE_API = 'private-location-write'; + const ruleTypes = [...UPTIME_RULE_TYPES, ...SYNTHETICS_RULE_TYPES]; const elasticManagedLocationsEnabledPrivilege: SubFeaturePrivilegeGroupConfig = { @@ -39,8 +41,8 @@ const elasticManagedLocationsEnabledPrivilege: SubFeaturePrivilegeGroupConfig = privileges: [ { id: 'elastic_managed_locations_enabled', - name: i18n.translate('xpack.synthetics.features.elasticManagedLocations', { - defaultMessage: 'Elastic managed locations enabled', + name: i18n.translate('xpack.synthetics.features.elasticManagedLocations.label', { + defaultMessage: 'Enabled', }), includeIn: 'all', savedObject: { @@ -52,6 +54,25 @@ const elasticManagedLocationsEnabledPrivilege: SubFeaturePrivilegeGroupConfig = ], }; +const canManagePrivateLocationsPrivilege: SubFeaturePrivilegeGroupConfig = { + groupType: 'independent' as SubFeaturePrivilegeGroupType, + privileges: [ + { + id: 'can_manage_private_locations', + name: i18n.translate('xpack.synthetics.features.canManagePrivateLocations', { + defaultMessage: 'Can manage', + }), + includeIn: 'all', + api: [PRIVATE_LOCATION_WRITE_API], + savedObject: { + all: [privateLocationSavedObjectName, legacyPrivateLocationsSavedObjectName], + read: [], + }, + ui: ['canManagePrivateLocations'], + }, + ], +}; + export const syntheticsFeature = { id: PLUGIN.ID, name: PLUGIN.NAME, @@ -74,13 +95,12 @@ export const syntheticsFeature = { syntheticsSettingsObjectType, syntheticsMonitorType, syntheticsApiKeyObjectType, - privateLocationSavedObjectName, - legacyPrivateLocationsSavedObjectName, syntheticsParamType, + // uptime settings object is also registered here since feature is shared between synthetics and uptime uptimeSettingsObjectType, ], - read: [], + read: [privateLocationSavedObjectName, legacyPrivateLocationsSavedObjectName], }, alerting: { rule: { @@ -128,10 +148,24 @@ export const syntheticsFeature = { }, subFeatures: [ { - name: i18n.translate('xpack.synthetics.features.app', { - defaultMessage: 'Synthetics', + name: i18n.translate('xpack.synthetics.features.app.elastic', { + defaultMessage: 'Elastic managed locations', + }), + description: i18n.translate('xpack.synthetics.features.app.elasticDescription', { + defaultMessage: + 'This feature enables users to create monitors that execute tests from Elastic managed infrastructure around the globe. There is an additional charge to use Elastic Managed testing locations. See the Elastic Cloud Pricing https://www.elastic.co/pricing page for current prices.', }), privilegeGroups: [elasticManagedLocationsEnabledPrivilege], }, + { + name: i18n.translate('xpack.synthetics.features.app.private', { + defaultMessage: 'Private locations', + }), + description: i18n.translate('xpack.synthetics.features.app.private,description', { + defaultMessage: + 'This feature allows you to manage your private locations, for example adding, or deleting them.', + }), + privilegeGroups: [canManagePrivateLocationsPrivilege], + }, ], }; diff --git a/x-pack/plugins/observability_solution/synthetics/server/lib.ts b/x-pack/plugins/observability_solution/synthetics/server/lib.ts index 94150c0fb8ee5..8a703e1c051e8 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/lib.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/lib.ts @@ -158,7 +158,7 @@ export class SyntheticsEsClient { esError, esRequestParams: { index: SYNTHETICS_INDEX_PATTERN, ...request }, esRequestStatus: RequestStatus.OK, - esResponse: res.body.responses[index], + esResponse: res?.body.responses[index], kibanaRequest: this.request!, operationName: operationName ?? '', startTime: startTimeNow, @@ -168,9 +168,10 @@ export class SyntheticsEsClient { } return { - responses: res.body?.responses as unknown as Array< - InferSearchResponseOf - >, + responses: + (res?.body?.responses as unknown as Array< + InferSearchResponseOf + >) ?? [], }; } diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/edit_monitor.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/edit_monitor.ts index d460b71037950..2b815313c79c9 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/edit_monitor.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/monitor_cruds/edit_monitor.ts @@ -305,7 +305,7 @@ export const syncEditedMonitor = async ({ }; export const validatePermissions = async ( - { server, response, request }: RouteContext, + routeContext: RouteContext, monitorLocations: MonitorLocations ) => { const hasPublicLocations = monitorLocations?.some((loc) => loc.isServiceManaged); @@ -313,19 +313,26 @@ export const validatePermissions = async ( return; } - const elasticManagedLocationsEnabled = - Boolean( - ( - await server.coreStart?.capabilities.resolveCapabilities(request, { - capabilityPath: 'uptime.*', - }) - ).uptime.elasticManagedLocationsEnabled - ) ?? true; + const { elasticManagedLocationsEnabled } = await validateLocationPermissions(routeContext); if (!elasticManagedLocationsEnabled) { return ELASTIC_MANAGED_LOCATIONS_DISABLED; } }; +export const validateLocationPermissions = async ({ server, request }: RouteContext) => { + const uptimeFeature = await server.coreStart?.capabilities.resolveCapabilities(request, { + capabilityPath: 'uptime.*', + }); + const elasticManagedLocationsEnabled = + Boolean(uptimeFeature.uptime.elasticManagedLocationsEnabled) ?? true; + const canManagePrivateLocations = Boolean(uptimeFeature.uptime.canManagePrivateLocations) ?? true; + + return { + canManagePrivateLocations, + elasticManagedLocationsEnabled, + }; +}; + const getInvalidOriginError = (monitor: SyntheticsMonitor) => { return { body: { diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/add_private_location.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/add_private_location.ts index 1feb120b2ea14..fa88de31e3ec8 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/add_private_location.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/add_private_location.ts @@ -6,6 +6,7 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; +import { PRIVATE_LOCATION_WRITE_API } from '../../../feature'; import { migrateLegacyPrivateLocations } from './migrate_legacy_private_locations'; import { SyntheticsRestApiRouteFactory } from '../../types'; import { getPrivateLocationsAndAgentPolicies } from './get_private_locations'; @@ -38,10 +39,12 @@ export const addPrivateLocationRoute: SyntheticsRestApiRouteFactory { - await migrateLegacyPrivateLocations(routeContext); + const { response, request, savedObjectsClient, syntheticsMonitorClient, server } = routeContext; + const internalSOClient = server.coreStart.savedObjects.createInternalRepository(); - const { response, request, savedObjectsClient, syntheticsMonitorClient } = routeContext; + await migrateLegacyPrivateLocations(internalSOClient, server.logger); const location = request.body as PrivateLocationObject; diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/delete_private_location.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/delete_private_location.ts index bac3907eac871..d01b255dd2b32 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/delete_private_location.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/delete_private_location.ts @@ -7,6 +7,7 @@ import { schema } from '@kbn/config-schema'; import { isEmpty } from 'lodash'; +import { PRIVATE_LOCATION_WRITE_API } from '../../../feature'; import { migrateLegacyPrivateLocations } from './migrate_legacy_private_locations'; import { getMonitorsByLocation } from './get_location_monitors'; import { getPrivateLocationsAndAgentPolicies } from './get_private_locations'; @@ -25,10 +26,13 @@ export const deletePrivateLocationRoute: SyntheticsRestApiRouteFactory { - await migrateLegacyPrivateLocations(routeContext); - const { savedObjectsClient, syntheticsMonitorClient, request, response, server } = routeContext; + const internalSOClient = server.coreStart.savedObjects.createInternalRepository(); + + await migrateLegacyPrivateLocations(internalSOClient, server.logger); + const { locationId } = request.params as { locationId: string }; const { locations } = await getPrivateLocationsAndAgentPolicies( diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/get_private_locations.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/get_private_locations.ts index d884bba5c2b0a..90d2e861379ad 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/get_private_locations.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/get_private_locations.ts @@ -31,9 +31,11 @@ export const getPrivateLocationsRoute: SyntheticsRestApiRouteFactory< }, }, handler: async (routeContext) => { - await migrateLegacyPrivateLocations(routeContext); + const { savedObjectsClient, syntheticsMonitorClient, request, response, server } = routeContext; + + const internalSOClient = server.coreStart.savedObjects.createInternalRepository(); + await migrateLegacyPrivateLocations(internalSOClient, server.logger); - const { savedObjectsClient, syntheticsMonitorClient, request, response } = routeContext; const { id } = request.params as { id?: string }; const { locations, agentPolicies } = await getPrivateLocationsAndAgentPolicies( diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/migrate_legacy_private_locations.test.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/migrate_legacy_private_locations.test.ts index 2305853aab3f1..f1e3101523c23 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/migrate_legacy_private_locations.test.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/migrate_legacy_private_locations.test.ts @@ -6,42 +6,28 @@ */ import { migrateLegacyPrivateLocations } from './migrate_legacy_private_locations'; -import { SyntheticsServerSetup } from '../../../types'; -import { coreMock, savedObjectsClientMock } from '@kbn/core/server/mocks'; +import { savedObjectsRepositoryMock } from '@kbn/core/server/mocks'; import { loggerMock } from '@kbn/logging-mocks'; -import { - type ISavedObjectsRepository, - SavedObjectsClientContract, -} from '@kbn/core-saved-objects-api-server'; +import { type ISavedObjectsRepository } from '@kbn/core-saved-objects-api-server'; +import { Logger } from '@kbn/logging'; describe('migrateLegacyPrivateLocations', () => { - let serverMock: SyntheticsServerSetup; - let savedObjectsClient: jest.Mocked; - let repositoryMock: ISavedObjectsRepository; + let loggerMockVal: Logger; + let repositoryMock: jest.Mocked; beforeEach(() => { - const coreStartMock = coreMock.createStart(); - serverMock = { - coreStart: coreStartMock, - logger: loggerMock.create(), - } as any; - savedObjectsClient = savedObjectsClientMock.create(); - repositoryMock = coreMock.createStart().savedObjects.createInternalRepository(); - - coreStartMock.savedObjects.createInternalRepository.mockReturnValue(repositoryMock); + repositoryMock = savedObjectsRepositoryMock.create(); + loggerMockVal = loggerMock.create(); }); it('should get the legacy private locations', async () => { - savedObjectsClient.get.mockResolvedValueOnce({ + repositoryMock.get.mockResolvedValueOnce({ attributes: { locations: [{ id: '1', label: 'Location 1' }] }, } as any); - savedObjectsClient.find.mockResolvedValueOnce({ total: 1 } as any); + repositoryMock.find.mockResolvedValueOnce({ total: 1 } as any); - await migrateLegacyPrivateLocations({ - server: serverMock, - savedObjectsClient, - } as any); + await migrateLegacyPrivateLocations(repositoryMock, loggerMockVal); - expect(savedObjectsClient.get).toHaveBeenCalledWith( + expect(repositoryMock.get).toHaveBeenCalledWith( 'synthetics-privates-locations', 'synthetics-privates-locations-singleton' ); @@ -49,43 +35,36 @@ describe('migrateLegacyPrivateLocations', () => { it('should log and return if an error occurs while getting legacy private locations', async () => { const error = new Error('Get error'); - savedObjectsClient.get.mockRejectedValueOnce(error); - - await migrateLegacyPrivateLocations({ - server: serverMock, - savedObjectsClient, + repositoryMock.get.mockResolvedValueOnce({ + attributes: { locations: [{ id: '1', label: 'Location 1' }] }, } as any); + repositoryMock.bulkCreate.mockRejectedValueOnce(error); + + await migrateLegacyPrivateLocations(repositoryMock, loggerMockVal); - expect(serverMock.logger.error).toHaveBeenCalledWith( - `Error getting legacy private locations: ${error}` + expect(loggerMockVal.error).toHaveBeenCalledWith( + 'Error migrating legacy private locations: Error: Get error' ); - expect(repositoryMock.bulkCreate).not.toHaveBeenCalled(); }); it('should return if there are no legacy locations', async () => { - savedObjectsClient.get.mockResolvedValueOnce({ + repositoryMock.get.mockResolvedValueOnce({ attributes: { locations: [] }, } as any); - await migrateLegacyPrivateLocations({ - server: serverMock, - savedObjectsClient: savedObjectsClientMock, - } as any); + await migrateLegacyPrivateLocations(repositoryMock, loggerMockVal); expect(repositoryMock.bulkCreate).not.toHaveBeenCalled(); }); it('should bulk create new private locations if there are legacy locations', async () => { const legacyLocations = [{ id: '1', label: 'Location 1' }]; - savedObjectsClient.get.mockResolvedValueOnce({ + repositoryMock.get.mockResolvedValueOnce({ attributes: { locations: legacyLocations }, } as any); - savedObjectsClient.find.mockResolvedValueOnce({ total: 1 } as any); + repositoryMock.find.mockResolvedValueOnce({ total: 1 } as any); - await migrateLegacyPrivateLocations({ - server: serverMock, - savedObjectsClient, - } as any); + await migrateLegacyPrivateLocations(repositoryMock, loggerMockVal); expect(repositoryMock.bulkCreate).toHaveBeenCalledWith( legacyLocations.map((location) => ({ @@ -100,17 +79,14 @@ describe('migrateLegacyPrivateLocations', () => { it('should delete legacy private locations if bulk create count matches', async () => { const legacyLocations = [{ id: '1', label: 'Location 1' }]; - savedObjectsClient.get.mockResolvedValueOnce({ + repositoryMock.get.mockResolvedValueOnce({ attributes: { locations: legacyLocations }, } as any); - savedObjectsClient.find.mockResolvedValueOnce({ total: 1 } as any); + repositoryMock.find.mockResolvedValueOnce({ total: 1 } as any); - await migrateLegacyPrivateLocations({ - server: serverMock, - savedObjectsClient, - } as any); + await migrateLegacyPrivateLocations(repositoryMock, loggerMockVal); - expect(savedObjectsClient.delete).toHaveBeenCalledWith( + expect(repositoryMock.delete).toHaveBeenCalledWith( 'synthetics-privates-locations', 'synthetics-privates-locations-singleton', {} diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/migrate_legacy_private_locations.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/migrate_legacy_private_locations.ts index cd73e27b950e3..e823e7764f540 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/migrate_legacy_private_locations.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/settings/private_locations/migrate_legacy_private_locations.ts @@ -6,6 +6,8 @@ */ import { SavedObject } from '@kbn/core-saved-objects-server'; +import type { ISavedObjectsRepository } from '@kbn/core-saved-objects-api-server'; +import { Logger } from '@kbn/logging'; import { type PrivateLocationAttributes, SyntheticsPrivateLocationsAttributes, @@ -15,21 +17,20 @@ import { legacyPrivateLocationsSavedObjectName, privateLocationSavedObjectName, } from '../../../../common/saved_objects/private_locations'; -import { RouteContext } from '../../types'; -export const migrateLegacyPrivateLocations = async ({ - server, - savedObjectsClient, -}: RouteContext) => { +export const migrateLegacyPrivateLocations = async ( + soClient: ISavedObjectsRepository, + logger: Logger +) => { try { let obj: SavedObject | undefined; try { - obj = await savedObjectsClient.get( + obj = await soClient.get( legacyPrivateLocationsSavedObjectName, legacyPrivateLocationsSavedObjectId ); } catch (e) { - server.logger.error(`Error getting legacy private locations: ${e}`); + // we don't need to do anything if the legacy object doesn't exist return; } const legacyLocations = obj?.attributes.locations ?? []; @@ -37,8 +38,6 @@ export const migrateLegacyPrivateLocations = async ({ return; } - const soClient = server.coreStart.savedObjects.createInternalRepository(); - await soClient.bulkCreate( legacyLocations.map((location) => ({ id: location.id, @@ -51,20 +50,21 @@ export const migrateLegacyPrivateLocations = async ({ } ); - const { total } = await savedObjectsClient.find({ + const { total } = await soClient.find({ type: privateLocationSavedObjectName, fields: [], perPage: 0, + namespaces: ['*'], }); if (total === legacyLocations.length) { - await savedObjectsClient.delete( + await soClient.delete( legacyPrivateLocationsSavedObjectName, legacyPrivateLocationsSavedObjectId, {} ); } } catch (e) { - server.logger.error(`Error migrating legacy private locations: ${e}`); + logger.error(`Error migrating legacy private locations: ${e}`); } }; diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/types.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/types.ts index fa481d16b92bf..837197d72d23a 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/types.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/types.ts @@ -38,6 +38,7 @@ export type SupportedMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'; export interface UMServerRoute { method: SupportedMethod; writeAccess?: boolean; + requiredPrivileges?: string[]; handler: T; validation?: VersionedRouteValidation; streamHandler?: ( diff --git a/x-pack/plugins/observability_solution/synthetics/server/runtime_types/private_locations.ts b/x-pack/plugins/observability_solution/synthetics/server/runtime_types/private_locations.ts index f695662e811e3..b7eaea482db92 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/runtime_types/private_locations.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/runtime_types/private_locations.ts @@ -21,6 +21,7 @@ export const PrivateLocationAttributesCodec = t.intersection([ lon: t.number, }), namespace: t.string, + spaces: t.array(t.string), }), ]); diff --git a/x-pack/plugins/observability_solution/synthetics/server/synthetics_route_wrapper.ts b/x-pack/plugins/observability_solution/synthetics/server/synthetics_route_wrapper.ts index 697cf93d74213..da3887ac94d56 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/synthetics_route_wrapper.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/synthetics_route_wrapper.ts @@ -24,7 +24,11 @@ export const syntheticsRouteWrapper: SyntheticsRouteWrapper = ( }, security: { authz: { - requiredPrivileges: ['uptime-read', ...(uptimeRoute?.writeAccess ? ['uptime-write'] : [])], + requiredPrivileges: [ + 'uptime-read', + ...(uptimeRoute.requiredPrivileges ?? []), + ...(uptimeRoute?.writeAccess ? ['uptime-write'] : []), + ], }, }, handler: async (context, request, response) => { diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 5e475680ccc1a..ad5ff96d1f97a 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -46250,7 +46250,6 @@ "xpack.synthetics.failedStep.label": "Étape ayant échoué", "xpack.synthetics.fcp.label": "FCP", "xpack.synthetics.featureRegistry.syntheticsFeatureName": "Synthetics et Uptime", - "xpack.synthetics.features.app": "Synthetics", "xpack.synthetics.features.elasticManagedLocations": "Emplacements gérés par Elastic activés", "xpack.synthetics.fieldLabels.cls": "Cumulative Layout Shift (CLS)", "xpack.synthetics.fieldLabels.dcl": "Événement DOMContentLoaded (DCL)", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 04783f99fec8a..19cba5d764169 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -46215,7 +46215,6 @@ "xpack.synthetics.failedStep.label": "失敗したステップ", "xpack.synthetics.fcp.label": "FCP", "xpack.synthetics.featureRegistry.syntheticsFeatureName": "Syntheticsとアップタイム", - "xpack.synthetics.features.app": "Synthetics", "xpack.synthetics.features.elasticManagedLocations": "Elasticで管理されている場所が有効です", "xpack.synthetics.fieldLabels.cls": "累積レイアウト変更(CLS)", "xpack.synthetics.fieldLabels.dcl": "DOMContentLoadedイベント(DCL)", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 4a2cf0e98a907..7414cfa4ceb93 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -45500,7 +45500,6 @@ "xpack.synthetics.failedStep.label": "失败的步骤", "xpack.synthetics.fcp.label": "FCP", "xpack.synthetics.featureRegistry.syntheticsFeatureName": "Synthetics 和 Uptime", - "xpack.synthetics.features.app": "Synthetics", "xpack.synthetics.features.elasticManagedLocations": "已启用 Elastic 托管位置", "xpack.synthetics.fieldLabels.cls": "累计布局偏移 (CLS)", "xpack.synthetics.fieldLabels.dcl": "DOMContentLoaded 事件 (DCL)", diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index b269aef6ae1cc..dc84f6c33d200 100644 --- a/x-pack/test/api_integration/apis/security/privileges.ts +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -91,7 +91,14 @@ export default function ({ getService }: FtrProviderContext) { 'execute_operations_all', 'scan_operations_all', ], - uptime: ['all', 'read', 'minimal_all', 'minimal_read', 'elastic_managed_locations_enabled'], + uptime: [ + 'all', + 'read', + 'minimal_all', + 'minimal_read', + 'elastic_managed_locations_enabled', + 'can_manage_private_locations', + ], securitySolutionAssistant: [ 'all', 'read', diff --git a/x-pack/test/api_integration/apis/security/privileges_basic.ts b/x-pack/test/api_integration/apis/security/privileges_basic.ts index a97ee360062c0..2bbd70fcf730d 100644 --- a/x-pack/test/api_integration/apis/security/privileges_basic.ts +++ b/x-pack/test/api_integration/apis/security/privileges_basic.ts @@ -178,6 +178,7 @@ export default function ({ getService }: FtrProviderContext) { ], uptime: [ 'all', + 'can_manage_private_locations', 'elastic_managed_locations_enabled', 'read', 'minimal_all', diff --git a/x-pack/test/api_integration/apis/synthetics/private_location_apis.ts b/x-pack/test/api_integration/apis/synthetics/private_location_apis.ts index 415c91af28347..a4351ede2eda0 100644 --- a/x-pack/test/api_integration/apis/synthetics/private_location_apis.ts +++ b/x-pack/test/api_integration/apis/synthetics/private_location_apis.ts @@ -11,16 +11,21 @@ import { legacyPrivateLocationsSavedObjectName, privateLocationSavedObjectName, } from '@kbn/synthetics-plugin/common/saved_objects/private_locations'; +import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants'; +import { PrivateLocation } from '@kbn/synthetics-plugin/common/runtime_types'; +import { SyntheticsMonitorTestService } from './services/synthetics_monitor_test_service'; import { FtrProviderContext } from '../../ftr_provider_context'; import { PrivateLocationTestService } from './services/private_location_test_service'; export default function ({ getService }: FtrProviderContext) { describe('PrivateLocationAPI', function () { this.tags('skipCloud'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); const kServer = getService('kibanaServer'); const testPrivateLocations = new PrivateLocationTestService(getService); + const monitorTestService = new SyntheticsMonitorTestService(getService); before(async () => { await testPrivateLocations.installSyntheticsPackage(); @@ -60,5 +65,72 @@ export default function ({ getService }: FtrProviderContext) { expect(e.response.status).to.be(404); } }); + + it('cannot create private location if privileges are missing', async () => { + const apiResponse = await testPrivateLocations.addFleetPolicy(); + const agentPolicyId = apiResponse.body.item.id; + + const { username, password } = await monitorTestService.addsNewSpace(['minimal_all']); + + const location: Omit = { + label: 'Test private location 10', + agentPolicyId: agentPolicyId!, + geo: { + lat: 0, + lon: 0, + }, + }; + const response = await supertestWithoutAuth + .post(SYNTHETICS_API_URLS.PRIVATE_LOCATIONS) + .auth(username, password) + .set('kbn-xsrf', 'true') + .send(location); + + expect(response.status).to.be(403); + }); + + it('can create private location if privileges are added', async () => { + const apiResponse = await testPrivateLocations.addFleetPolicy(); + const agentPolicyId = apiResponse.body.item.id; + + const { username, password } = await monitorTestService.addsNewSpace([ + 'minimal_all', + 'can_manage_private_locations', + ]); + + const location: Omit = { + label: 'Test private location 10', + agentPolicyId: agentPolicyId!, + geo: { + lat: 0, + lon: 0, + }, + }; + const response = await supertestWithoutAuth + .post(SYNTHETICS_API_URLS.PRIVATE_LOCATIONS) + .auth(username, password) + .set('kbn-xsrf', 'true') + .send(location); + + expect(response.status).to.be(200); + }); + + it('can delete private location if privileges are added', async () => { + const locs = await testPrivateLocations.fetchAll(); + + const { username, password } = await monitorTestService.addsNewSpace([ + 'minimal_all', + 'can_manage_private_locations', + ]); + + for (const loc of locs.body) { + const deleteResponse = await supertestWithoutAuth + .delete(`${SYNTHETICS_API_URLS.PRIVATE_LOCATIONS}/${loc.id}`) + .auth(username, password) + .set('kbn-xsrf', 'true'); + + expect(deleteResponse.status).to.be(200); + } + }); }); } diff --git a/x-pack/test/api_integration/apis/synthetics/synthetics_api_security.ts b/x-pack/test/api_integration/apis/synthetics/synthetics_api_security.ts index ddd0d4b209940..3969dcae88219 100644 --- a/x-pack/test/api_integration/apis/synthetics/synthetics_api_security.ts +++ b/x-pack/test/api_integration/apis/synthetics/synthetics_api_security.ts @@ -33,12 +33,18 @@ export default function ({ getService }: FtrProviderContext) { password: string; writeAccess?: boolean; tags?: string; + readUser?: boolean; } ) => { let resp; - const { statusCodes, SPACE_ID, username, password, writeAccess } = options; - const tags = !writeAccess ? '[uptime-read]' : options.tags ?? '[uptime-read,uptime-write]'; - const getStatusMessage = (respStatus: string) => + const { statusCodes, SPACE_ID, username, password, writeAccess, readUser } = options; + let tags = !writeAccess ? '[uptime-read]' : options.tags ?? '[uptime-read,uptime-write]'; + if ((method === 'POST' || method === 'DELETE') && path.includes('private_locations')) { + tags = readUser + ? '[private-location-write,uptime-write]' + : '[uptime-read,private-location-write,uptime-write]'; + } + const getStatusMessage = (respStatus: string | number) => `Expected ${statusCodes?.join( ',' )}, got ${respStatus} status code doesn't match, for path: ${path} and method ${method}`; @@ -132,6 +138,7 @@ export default function ({ getService }: FtrProviderContext) { password, writeAccess: route.writeAccess ?? true, tags: '[uptime-write]', + readUser: true, }); } }); diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/platform_security/authorization.ts b/x-pack/test_serverless/api_integration/test_suites/observability/platform_security/authorization.ts index e49a9ed45871e..c77d5041aba06 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/platform_security/authorization.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/platform_security/authorization.ts @@ -8684,6 +8684,7 @@ export default function ({ getService }: FtrProviderContext) { "api:uptime-write", "api:lists-all", "api:rac", + "api:private-location-write", "app:uptime", "app:kibana", "app:synthetics", @@ -8728,30 +8729,6 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:uptime-synthetics-api-key/delete", "saved_object:uptime-synthetics-api-key/bulk_delete", "saved_object:uptime-synthetics-api-key/share_to_space", - "saved_object:synthetics-private-location/bulk_get", - "saved_object:synthetics-private-location/get", - "saved_object:synthetics-private-location/find", - "saved_object:synthetics-private-location/open_point_in_time", - "saved_object:synthetics-private-location/close_point_in_time", - "saved_object:synthetics-private-location/create", - "saved_object:synthetics-private-location/bulk_create", - "saved_object:synthetics-private-location/update", - "saved_object:synthetics-private-location/bulk_update", - "saved_object:synthetics-private-location/delete", - "saved_object:synthetics-private-location/bulk_delete", - "saved_object:synthetics-private-location/share_to_space", - "saved_object:synthetics-privates-locations/bulk_get", - "saved_object:synthetics-privates-locations/get", - "saved_object:synthetics-privates-locations/find", - "saved_object:synthetics-privates-locations/open_point_in_time", - "saved_object:synthetics-privates-locations/close_point_in_time", - "saved_object:synthetics-privates-locations/create", - "saved_object:synthetics-privates-locations/bulk_create", - "saved_object:synthetics-privates-locations/update", - "saved_object:synthetics-privates-locations/bulk_update", - "saved_object:synthetics-privates-locations/delete", - "saved_object:synthetics-privates-locations/bulk_delete", - "saved_object:synthetics-privates-locations/share_to_space", "saved_object:synthetics-param/bulk_get", "saved_object:synthetics-param/get", "saved_object:synthetics-param/find", @@ -8788,6 +8765,30 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:telemetry/delete", "saved_object:telemetry/bulk_delete", "saved_object:telemetry/share_to_space", + "saved_object:synthetics-private-location/bulk_get", + "saved_object:synthetics-private-location/get", + "saved_object:synthetics-private-location/find", + "saved_object:synthetics-private-location/open_point_in_time", + "saved_object:synthetics-private-location/close_point_in_time", + "saved_object:synthetics-private-location/create", + "saved_object:synthetics-private-location/bulk_create", + "saved_object:synthetics-private-location/update", + "saved_object:synthetics-private-location/bulk_update", + "saved_object:synthetics-private-location/delete", + "saved_object:synthetics-private-location/bulk_delete", + "saved_object:synthetics-private-location/share_to_space", + "saved_object:synthetics-privates-locations/bulk_get", + "saved_object:synthetics-privates-locations/get", + "saved_object:synthetics-privates-locations/find", + "saved_object:synthetics-privates-locations/open_point_in_time", + "saved_object:synthetics-privates-locations/close_point_in_time", + "saved_object:synthetics-privates-locations/create", + "saved_object:synthetics-privates-locations/bulk_create", + "saved_object:synthetics-privates-locations/update", + "saved_object:synthetics-privates-locations/bulk_update", + "saved_object:synthetics-privates-locations/delete", + "saved_object:synthetics-privates-locations/bulk_delete", + "saved_object:synthetics-privates-locations/share_to_space", "saved_object:config/bulk_get", "saved_object:config/get", "saved_object:config/find", @@ -8808,6 +8809,7 @@ export default function ({ getService }: FtrProviderContext) { "ui:uptime/show", "ui:uptime/alerting:save", "ui:uptime/elasticManagedLocationsEnabled", + "ui:uptime/canManagePrivateLocations", "alerting:xpack.uptime.alerts.tls/uptime/rule/get", "alerting:xpack.uptime.alerts.tls/uptime/rule/getRuleState", "alerting:xpack.uptime.alerts.tls/uptime/rule/getAlertSummary", @@ -9375,6 +9377,35 @@ export default function ({ getService }: FtrProviderContext) { "alerting:xpack.synthetics.alerts.tls/observability/alert/getAlertSummary", "alerting:xpack.synthetics.alerts.tls/observability/alert/update", ], + "can_manage_private_locations": Array [ + "login:", + "api:private-location-write", + "saved_object:synthetics-private-location/bulk_get", + "saved_object:synthetics-private-location/get", + "saved_object:synthetics-private-location/find", + "saved_object:synthetics-private-location/open_point_in_time", + "saved_object:synthetics-private-location/close_point_in_time", + "saved_object:synthetics-private-location/create", + "saved_object:synthetics-private-location/bulk_create", + "saved_object:synthetics-private-location/update", + "saved_object:synthetics-private-location/bulk_update", + "saved_object:synthetics-private-location/delete", + "saved_object:synthetics-private-location/bulk_delete", + "saved_object:synthetics-private-location/share_to_space", + "saved_object:synthetics-privates-locations/bulk_get", + "saved_object:synthetics-privates-locations/get", + "saved_object:synthetics-privates-locations/find", + "saved_object:synthetics-privates-locations/open_point_in_time", + "saved_object:synthetics-privates-locations/close_point_in_time", + "saved_object:synthetics-privates-locations/create", + "saved_object:synthetics-privates-locations/bulk_create", + "saved_object:synthetics-privates-locations/update", + "saved_object:synthetics-privates-locations/bulk_update", + "saved_object:synthetics-privates-locations/delete", + "saved_object:synthetics-privates-locations/bulk_delete", + "saved_object:synthetics-privates-locations/share_to_space", + "ui:uptime/canManagePrivateLocations", + ], "elastic_managed_locations_enabled": Array [ "login:", "ui:uptime/elasticManagedLocationsEnabled", @@ -9429,30 +9460,6 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:uptime-synthetics-api-key/delete", "saved_object:uptime-synthetics-api-key/bulk_delete", "saved_object:uptime-synthetics-api-key/share_to_space", - "saved_object:synthetics-private-location/bulk_get", - "saved_object:synthetics-private-location/get", - "saved_object:synthetics-private-location/find", - "saved_object:synthetics-private-location/open_point_in_time", - "saved_object:synthetics-private-location/close_point_in_time", - "saved_object:synthetics-private-location/create", - "saved_object:synthetics-private-location/bulk_create", - "saved_object:synthetics-private-location/update", - "saved_object:synthetics-private-location/bulk_update", - "saved_object:synthetics-private-location/delete", - "saved_object:synthetics-private-location/bulk_delete", - "saved_object:synthetics-private-location/share_to_space", - "saved_object:synthetics-privates-locations/bulk_get", - "saved_object:synthetics-privates-locations/get", - "saved_object:synthetics-privates-locations/find", - "saved_object:synthetics-privates-locations/open_point_in_time", - "saved_object:synthetics-privates-locations/close_point_in_time", - "saved_object:synthetics-privates-locations/create", - "saved_object:synthetics-privates-locations/bulk_create", - "saved_object:synthetics-privates-locations/update", - "saved_object:synthetics-privates-locations/bulk_update", - "saved_object:synthetics-privates-locations/delete", - "saved_object:synthetics-privates-locations/bulk_delete", - "saved_object:synthetics-privates-locations/share_to_space", "saved_object:synthetics-param/bulk_get", "saved_object:synthetics-param/get", "saved_object:synthetics-param/find", @@ -9489,6 +9496,16 @@ export default function ({ getService }: FtrProviderContext) { "saved_object:telemetry/delete", "saved_object:telemetry/bulk_delete", "saved_object:telemetry/share_to_space", + "saved_object:synthetics-private-location/bulk_get", + "saved_object:synthetics-private-location/get", + "saved_object:synthetics-private-location/find", + "saved_object:synthetics-private-location/open_point_in_time", + "saved_object:synthetics-private-location/close_point_in_time", + "saved_object:synthetics-privates-locations/bulk_get", + "saved_object:synthetics-privates-locations/get", + "saved_object:synthetics-privates-locations/find", + "saved_object:synthetics-privates-locations/open_point_in_time", + "saved_object:synthetics-privates-locations/close_point_in_time", "saved_object:config/bulk_get", "saved_object:config/get", "saved_object:config/find",