diff --git a/x-pack/plugins/synthetics/e2e/journeys/synthetics/private_locations.journey.ts b/x-pack/plugins/synthetics/e2e/journeys/synthetics/private_locations.journey.ts index 8cb69f498b066..e9aec6dc3f1c4 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/synthetics/private_locations.journey.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/synthetics/private_locations.journey.ts @@ -149,4 +149,20 @@ journey(`PrivateLocationsSettings`, async ({ page, params }) => { await page.click('button:has-text("Delete location")'); await page.click('text=Create your first private location'); }); + + step('login with non super user', async () => { + await page.click('[data-test-subj="userMenuAvatar"]'); + await page.click('text="Log out"'); + await syntheticsApp.loginToKibana('viewer', 'changeme'); + }); + + step('viewer user cannot add locations', async () => { + await syntheticsApp.navigateToSettings(false); + await page.click('text=Private Locations'); + await page.waitForSelector( + `text="You're missing some Kibana privileges to manage private locations"` + ); + const createLocationBtn = await page.getByRole('button', { name: 'Create location' }); + expect(await createLocationBtn.getAttribute('disabled')).toEqual(''); + }); }); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/components/permissions.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/components/permissions.tsx index e9094bbc9a3f0..97a9ac3e62ce3 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/components/permissions.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/components/permissions.tsx @@ -6,13 +6,14 @@ */ import React, { ReactNode } from 'react'; -import { EuiCallOut, EuiToolTip } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiCallOut, EuiToolTip, EuiCode } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; export const FleetPermissionsCallout = () => { return ( - -

{NEED_FLEET_READ_AGENT_POLICIES_PERMISSION}

+ +

{NEED_PRIVATE_LOCATIONS_PERMISSION}

); }; @@ -62,26 +63,32 @@ function getRestrictionReasonLabel( : undefined; } -export const NEED_PERMISSIONS = i18n.translate( - 'xpack.synthetics.monitorManagement.needPermissions', +export const NEED_PERMISSIONS_PRIVATE_LOCATIONS = i18n.translate( + 'xpack.synthetics.monitorManagement.privateLocations.needPermissions', { - defaultMessage: 'Need permissions', + defaultMessage: "You're missing some Kibana privileges to manage private locations", } ); -export const NEED_FLEET_READ_AGENT_POLICIES_PERMISSION = i18n.translate( - 'xpack.synthetics.monitorManagement.needFleetReadAgentPoliciesPermission', - { - defaultMessage: - 'You are not authorized to access Fleet. Fleet permissions are required to create new private locations.', - } +export const ALL = i18n.translate('xpack.synthetics.monitorManagement.priviledges.all', { + defaultMessage: 'All', +}); + +export const NEED_PRIVATE_LOCATIONS_PERMISSION = ( + {`"${ALL}"`}, + }} + /> ); export const CANNOT_SAVE_INTEGRATION_LABEL = i18n.translate( 'xpack.synthetics.monitorManagement.cannotSaveIntegration', { defaultMessage: - 'You are not authorized to update integrations. Integrations write permissions are required.', + 'You are not authorized to manage private locations. It requires the "All" Kibana privilege for both Fleet and Integrations.', } ); @@ -89,7 +96,7 @@ const CANNOT_PERFORM_ACTION_FLEET = i18n.translate( 'xpack.synthetics.monitorManagement.noFleetPermission', { defaultMessage: - 'You are not authorized to perform this action. Integrations write permissions are required.', + 'You are not authorized to perform this action. It requires the "All" Kibana privilege for Integrations.', } ); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/add_location_flyout.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/add_location_flyout.tsx index c87aa157eeffa..51e4cce0f2fb0 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/add_location_flyout.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/add_location_flyout.tsx @@ -19,7 +19,7 @@ import { EuiButton, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useFleetPermissions } from '../../../hooks/use_fleet_permissions'; +import { useCanManagePrivateLocation } from '../../../hooks/use_fleet_permissions'; import { useFormWrapped } from '../../../../../hooks/use_form_wrapped'; import { PrivateLocation } from '../../../../../../common/runtime_types'; import { FleetPermissionsCallout } from '../../common/components/permissions'; @@ -54,7 +54,7 @@ export const AddLocationFlyout = ({ const { handleSubmit } = form; - const { canReadAgentPolicies } = useFleetPermissions(); + const canManagePrivateLocation = useCanManagePrivateLocation(); const closeFlyout = () => { setIsOpen(false); @@ -69,9 +69,12 @@ export const AddLocationFlyout = ({ - {!canReadAgentPolicies && } + {!canManagePrivateLocation && } - + diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/agent_policy_needed.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/agent_policy_needed.tsx index 42aa5e8208ece..b3ad93e26d041 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/agent_policy_needed.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/agent_policy_needed.tsx @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import { useSyntheticsSettingsContext } from '../../../contexts'; import { LEARN_MORE, READ_DOCS } from './empty_locations'; -export const AgentPolicyNeeded = () => { +export const AgentPolicyNeeded = ({ disabled }: { disabled: boolean }) => { const { basePath } = useSyntheticsSettingsContext(); return ( @@ -20,7 +20,12 @@ export const AgentPolicyNeeded = () => { title={

{AGENT_POLICY_NEEDED}

} body={

{ADD_AGENT_POLICY_DESCRIPTION}

} actions={ - + {CREATE_AGENT_POLICY} } diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/delete_location.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/delete_location.tsx index 6d2c95cac70ae..959520c911469 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/delete_location.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/delete_location.tsx @@ -9,7 +9,7 @@ import React, { useState } from 'react'; import { EuiButtonIcon, EuiConfirmModal, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useSyntheticsSettingsContext } from '../../../contexts'; -import { useFleetPermissions } from '../../../hooks'; +import { useFleetPermissions, useCanManagePrivateLocation } from '../../../hooks'; import { CANNOT_SAVE_INTEGRATION_LABEL } from '../../common/components/permissions'; export const DeleteLocation = ({ @@ -30,6 +30,7 @@ export const DeleteLocation = ({ const { canSave } = useSyntheticsSettingsContext(); const { canSaveIntegrations } = useFleetPermissions(); + const canManagePrivateLocation = useCanManagePrivateLocation(); const [isModalOpen, setIsModalOpen] = useState(false); @@ -62,7 +63,9 @@ export const DeleteLocation = ({ return ( <> {isModalOpen && deleteModal} - + { setIsModalOpen(true); }} - isDisabled={!canDelete || !canSave} + isDisabled={!canDelete || !canManagePrivateLocation || !canSave} /> diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/location_form.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/location_form.tsx index 89fe466d241f1..38bdac2592fb7 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/location_form.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/location_form.tsx @@ -26,9 +26,11 @@ import { selectAgentPolicies } from '../../../state/private_locations'; export const LocationForm = ({ privateLocations, + hasPermissions, }: { onDiscard?: () => void; privateLocations: PrivateLocation[]; + hasPermissions: boolean; }) => { const { data } = useSelector(selectAgentPolicies); const { control, register } = useFormContext(); @@ -41,7 +43,7 @@ export const LocationForm = ({ return ( <> - {data?.items.length === 0 && } + {data?.items.length === 0 && } { const tags = item.tags || []; @@ -133,10 +133,10 @@ export const PrivateLocationsTable = ({ fill data-test-subj={'addPrivateLocationButton'} isLoading={loading} - disabled={!canSaveIntegrations || !canSave} + disabled={!canManagePrivateLocations || !canSave} onClick={() => setIsAddingNew(true)} iconType="plusInCircle" - title={!canSaveIntegrations ? CANNOT_SAVE_INTEGRATION_LABEL : undefined} + title={!canManagePrivateLocations ? CANNOT_SAVE_INTEGRATION_LABEL : undefined} > {ADD_LABEL} , diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/manage_empty_state.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/manage_empty_state.tsx index 9cc313a106c96..92768f48a83ef 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/manage_empty_state.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/manage_empty_state.tsx @@ -20,7 +20,7 @@ export const ManageEmptyState: FC<{ const { data: agentPolicies } = useSelector(selectAgentPolicies); if (agentPolicies?.total === 0) { - return ; + return ; } if (privateLocations.length === 0) { diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/manage_private_locations.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/manage_private_locations.test.tsx new file mode 100644 index 0000000000000..b4e406353f2a1 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/manage_private_locations.test.tsx @@ -0,0 +1,169 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '../../../utils/testing/rtl_helpers'; +import * as permissionsHooks from '../../../hooks'; +import * as locationHooks from './hooks/use_locations_api'; +import * as settingsHooks from '../../../contexts/synthetics_settings_context'; +import type { SyntheticsSettingsContextValues } from '../../../contexts'; +import { ManagePrivateLocations } from './manage_private_locations'; +import { PrivateLocation } from '../../../../../../common/runtime_types'; + +jest.mock('../../../hooks'); +jest.mock('./hooks/use_locations_api'); +jest.mock('../../../contexts/synthetics_settings_context'); + +describe('', () => { + beforeEach(() => { + jest.spyOn(permissionsHooks, 'useCanManagePrivateLocation').mockReturnValue(true); + jest.spyOn(locationHooks, 'useLocationsAPI').mockReturnValue({ + formData: {} as PrivateLocation, + loading: false, + onSubmit: jest.fn(), + privateLocations: [], + onDelete: jest.fn(), + deleteLoading: false, + }); + jest.spyOn(settingsHooks, 'useSyntheticsSettingsContext').mockReturnValue({ + canSave: true, + } as SyntheticsSettingsContextValues); + }); + + it.each([true, false])( + 'handles no agent found when the user does and does not have permissions', + (hasFleetPermissions) => { + jest + .spyOn(permissionsHooks, 'useCanManagePrivateLocation') + .mockReturnValue(hasFleetPermissions); + const { getByText, getByRole, queryByText } = render(, { + state: { + agentPolicies: { + data: { + items: [], + total: 0, + page: 1, + perPage: 20, + }, + loading: false, + error: null, + isManageFlyoutOpen: false, + isAddingNewPrivateLocation: false, + }, + }, + }); + expect(getByText('No agent policies found')).toBeInTheDocument(); + + if (hasFleetPermissions) { + const button = getByRole('link', { name: 'Create agent policy' }); + expect(button).not.toBeDisabled(); + expect( + queryByText(/You are not authorized to manage private locations./) + ).not.toBeInTheDocument(); + } else { + const button = getByRole('button', { name: 'Create agent policy' }); + expect(button).toBeDisabled(); + expect(getByText(/You are not authorized to manage private locations./)); + } + } + ); + + it.each([true, false])( + 'handles create first location when the user does and does not have permissions', + (hasFleetPermissions) => { + jest + .spyOn(permissionsHooks, 'useCanManagePrivateLocation') + .mockReturnValue(hasFleetPermissions); + const { getByText, getByRole, queryByText } = render(, { + state: { + agentPolicies: { + data: { + items: [{}], + total: 1, + page: 1, + perPage: 20, + }, + loading: false, + error: null, + isManageFlyoutOpen: false, + isAddingNewPrivateLocation: false, + }, + }, + }); + expect(getByText('Create your first private location')).toBeInTheDocument(); + const button = getByRole('button', { name: 'Create location' }); + + if (hasFleetPermissions) { + expect(button).not.toBeDisabled(); + expect( + queryByText(/You are not authorized to manage private locations./) + ).not.toBeInTheDocument(); + } else { + expect(button).toBeDisabled(); + expect(getByText(/You are not authorized to manage private locations./)); + } + } + ); + + it.each([true, false])( + 'handles location table when the user does and does not have permissions', + (hasFleetPermissions) => { + const privateLocationName = 'Test private location'; + jest + .spyOn(permissionsHooks, 'useCanManagePrivateLocation') + .mockReturnValue(hasFleetPermissions); + jest.spyOn(permissionsHooks, 'useFleetPermissions').mockReturnValue({ + canSaveIntegrations: hasFleetPermissions, + canReadAgentPolicies: hasFleetPermissions, + }); + jest.spyOn(locationHooks, 'useLocationsAPI').mockReturnValue({ + formData: {} as PrivateLocation, + loading: false, + onSubmit: jest.fn(), + privateLocations: [ + { + label: privateLocationName, + id: 'lkjlere', + agentPolicyId: 'lkjelrje', + isServiceManaged: false, + concurrentMonitors: 2, + }, + ], + onDelete: jest.fn(), + deleteLoading: false, + }); + const { getByText, getByRole, queryByText } = render(, { + state: { + agentPolicies: { + data: { + items: [{}], + total: 1, + page: 1, + perPage: 20, + }, + loading: false, + error: null, + isManageFlyoutOpen: false, + isAddingNewPrivateLocation: false, + }, + }, + }); + expect(getByText(privateLocationName)).toBeInTheDocument(); + const button = getByRole('button', { name: 'Create location' }); + + if (hasFleetPermissions) { + expect(button).not.toBeDisabled(); + expect( + queryByText(/You are not authorized to manage private locations./) + ).not.toBeInTheDocument(); + } else { + expect(button).toBeDisabled(); + expect(getByText(/You are not authorized to manage private locations./)); + } + } + ); +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/manage_private_locations.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/manage_private_locations.tsx index d697d011e5841..dd140ffd3b3f5 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/manage_private_locations.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/settings/private_locations/manage_private_locations.tsx @@ -6,9 +6,10 @@ */ import React, { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import { EuiSpacer } from '@elastic/eui'; import { LoadingState } from '../../monitors_page/overview/overview/monitor_detail_flyout'; import { PrivateLocationsTable } from './locations_table'; -import { useFleetPermissions } from '../../../hooks'; +import { useCanManagePrivateLocation } from '../../../hooks'; import { ManageEmptyState } from './manage_empty_state'; import { AddLocationFlyout } from './add_location_flyout'; import { useLocationsAPI } from './hooks/use_locations_api'; @@ -30,7 +31,7 @@ export const ManagePrivateLocations = () => { const { onSubmit, loading, privateLocations, onDelete, deleteLoading } = useLocationsAPI(); - const { canReadAgentPolicies } = useFleetPermissions(); + const canManagePrivateLocation = useCanManagePrivateLocation(); useEffect(() => { dispatch(getAgentPoliciesAction.get()); @@ -43,7 +44,12 @@ export const ManagePrivateLocations = () => { return ( <> - {!canReadAgentPolicies && } + {!canManagePrivateLocation && ( + <> + + + + )} {loading ? ( @@ -51,7 +57,7 @@ export const ManagePrivateLocations = () => { { return ( -

- {canReadAgentPolicies && ( - - {policy ? ( - - {policy?.name} - - ) : ( - - {POLICY_IS_DELETED} - - )} - - )} -

+ {canReadAgentPolicies && ( + + {policy ? ( + + {policy?.name} + + ) : ( + + {POLICY_IS_DELETED} + + )} + + )}
); }; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_fleet_permissions.ts b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_fleet_permissions.ts index bbd5aa4f681bf..2ed8af08891ab 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_fleet_permissions.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_fleet_permissions.ts @@ -31,6 +31,12 @@ export function useCanUpdatePrivateMonitor(monitor: EncryptedSyntheticsMonitor) return canUpdatePrivateMonitor(monitor, canSaveIntegrations); } +export function useCanManagePrivateLocation() { + const { canSaveIntegrations, canReadAgentPolicies } = useFleetPermissions(); + + return Boolean(canSaveIntegrations && canReadAgentPolicies); +} + export function canUpdatePrivateMonitor( monitor: EncryptedSyntheticsMonitor, canSaveIntegrations: boolean diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/manage_locations/manage_locations_flyout.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/manage_locations/manage_locations_flyout.tsx index 103a9a37480db..805097312d4e8 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/manage_locations/manage_locations_flyout.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/manage_locations/manage_locations_flyout.tsx @@ -164,7 +164,7 @@ export const NEED_PERMISSIONS = i18n.translate( ); export const NEED_FLEET_READ_AGENT_POLICIES_PERMISSION = i18n.translate( - 'xpack.synthetics.monitorManagement.needFleetReadAgentPoliciesPermission', + 'xpack.synthetics.monitorManagement.needFleetReadAgentPoliciesPermissionUptime', { defaultMessage: 'You are not authorized to access Fleet. Fleet permissions are required to create new private locations.', diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_config/locations.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_config/locations.tsx index 11e2588e909b7..3bcaa51c48f77 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_config/locations.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_config/locations.tsx @@ -146,7 +146,7 @@ export const INVALID_LABEL = i18n.translate('xpack.synthetics.monitorManagement. }); export const CANNOT_SAVE_INTEGRATION_LABEL = i18n.translate( - 'xpack.synthetics.monitorManagement.cannotSaveIntegration', + 'xpack.synthetics.monitorManagement.cannotSaveIntegrationUptime', { defaultMessage: 'You are not authorized to update integrations. Integrations write permissions are required.', diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 54d13791fa0d3..bc8fc835911fb 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -33706,7 +33706,6 @@ "xpack.synthetics.monitorManagement.monitorSync.failure.statusLabel": "Statut", "xpack.synthetics.monitorManagement.monitorSync.failure.title": "Impossible de synchroniser les moniteurs avec le service Synthetics", "xpack.synthetics.monitorManagement.nameRequired": "Le nom de l’emplacement est requis", - "xpack.synthetics.monitorManagement.needFleetReadAgentPoliciesPermission": "Vous n'êtes pas autorisé à accéder à Fleet. Des autorisations Fleet sont nécessaires pour créer de nouveaux emplacements privés.", "xpack.synthetics.monitorManagement.needPermissions": "Permissions requises", "xpack.synthetics.monitorManagement.new.label": "Nouveauté", "xpack.synthetics.monitorManagement.noLabel": "Annuler", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 774b9a54f191a..4bd272febf95a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -33677,7 +33677,6 @@ "xpack.synthetics.monitorManagement.monitorSync.failure.statusLabel": "ステータス", "xpack.synthetics.monitorManagement.monitorSync.failure.title": "モニターをSyntheticsサービスと同期できませんでした", "xpack.synthetics.monitorManagement.nameRequired": "場所名は必須です", - "xpack.synthetics.monitorManagement.needFleetReadAgentPoliciesPermission": "Fleet へのアクセスが許可されていません。新しい非公開の場所を作成するには、Fleet権限が必要です。", "xpack.synthetics.monitorManagement.needPermissions": "権限が必要です", "xpack.synthetics.monitorManagement.new.label": "新規", "xpack.synthetics.monitorManagement.noLabel": "キャンセル", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 27e7ec4722827..d1d3729f26079 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -33712,7 +33712,6 @@ "xpack.synthetics.monitorManagement.monitorSync.failure.statusLabel": "状态", "xpack.synthetics.monitorManagement.monitorSync.failure.title": "监测无法与 Synthetics 服务同步", "xpack.synthetics.monitorManagement.nameRequired": "“位置名称”必填", - "xpack.synthetics.monitorManagement.needFleetReadAgentPoliciesPermission": "您无权访问 Fleet。需要 Fleet 权限才能创建新的专用位置。", "xpack.synthetics.monitorManagement.needPermissions": "需要权限", "xpack.synthetics.monitorManagement.new.label": "新建", "xpack.synthetics.monitorManagement.noLabel": "取消",