Skip to content

Commit

Permalink
[Synthetics] Sub-feature for managing private locations !! (elastic#2…
Browse files Browse the repository at this point in the history
…01100)

## Summary

Fixes elastic#200899

Added Synthetics Sub-feature for managing private locations !!

User can configure a sub feature in a role under synthetics to allow
managing private locations ,

with role API it will be with 
```
{
      kibana: [
        {
          feature: {
            uptime: [
              'minimal_all',
              'can_manage_private_locations',
            ],
          },
          spaces: ['*'],
        },
      ],
    }
```



<img width="1728" alt="image"
src="https://github.com/user-attachments/assets/f842da22-9c82-43d0-ad34-c6e19ea187c6">

Create/delete actions on UI will be disabled 

<img width="1728" alt="image"
src="https://github.com/user-attachments/assets/1a164d85-357b-42f3-ae15-0682b2db6c75">

## Release note
If you have already modified sub feature for the synthetics/uptime
feature to disable user access for using elastic managed location,
addition of this feature means, they will also will not be able to
manage(add/delete) private locations. Though this will not impact usage
of private locations in monitors. If you want those users to have
ability to add/delete new private locations, you can enable that by
toggle this feature in role ui or via api.
  • Loading branch information
shahzad31 authored Dec 2, 2024
1 parent 3daaaa5 commit 93eedff
Show file tree
Hide file tree
Showing 24 changed files with 307 additions and 151 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 || [];
Expand Down Expand Up @@ -128,13 +128,16 @@ export const PrivateLocationsTable = ({

const renderToolRight = () => {
return [
<NoPermissionsTooltip canEditSynthetics={canSave}>
<NoPermissionsTooltip
canEditSynthetics={canSave}
canManagePrivateLocations={canManagePrivateLocations}
key="addPrivateLocationButton"
>
<EuiButton
key="addPrivateLocationButton"
fill
data-test-subj={'addPrivateLocationButton'}
isLoading={loading}
disabled={!canSave}
disabled={!canSave || !canManagePrivateLocations}
onClick={() => setIsAddingNew(true)}
iconType="plusInCircle"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ describe('<ManagePrivateLocations />', () => {
const privateLocationName = 'Test private location';
jest.spyOn(settingsHooks, 'useSyntheticsSettingsContext').mockReturnValue({
canSave,
canManagePrivateLocations: true,
} as SyntheticsSettingsContextValues);

jest.spyOn(locationHooks, 'usePrivateLocationsAPI').mockReturnValue({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export interface SyntheticsAppProps {

export interface SyntheticsSettingsContextValues {
canSave: boolean;
canManagePrivateLocations: boolean;
basePath: string;
dateRangeStart: string;
dateRangeEnd: string;
Expand Down Expand Up @@ -74,6 +75,7 @@ const defaultContext: SyntheticsSettingsContextValues = {
isLogsAvailable: true,
isDev: false,
canSave: false,
canManagePrivateLocations: false,
};
export const SyntheticsSettingsContext = createContext(defaultContext);

Expand All @@ -96,6 +98,8 @@ export const SyntheticsSettingsContextProvider: React.FC<PropsWithChildren<Synth
const { application } = useKibana().services;

const canSave = (application?.capabilities.uptime.save ?? false) as boolean;
const canManagePrivateLocations = (application?.capabilities.uptime.canManagePrivateLocations ??
false) as boolean;

const value = useMemo(() => {
return {
Expand All @@ -109,6 +113,7 @@ export const SyntheticsSettingsContextProvider: React.FC<PropsWithChildren<Synth
dateRangeStart: dateRangeStart ?? DATE_RANGE_START,
dateRangeEnd: dateRangeEnd ?? DATE_RANGE_END,
isServerless,
canManagePrivateLocations,
};
}, [
canSave,
Expand All @@ -121,6 +126,7 @@ export const SyntheticsSettingsContextProvider: React.FC<PropsWithChildren<Synth
dateRangeEnd,
commonlyUsedRanges,
isServerless,
canManagePrivateLocations,
]);

return <SyntheticsSettingsContext.Provider value={value} children={children} />;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ describe('useBreadcrumbs', () => {
setBreadcrumbs: core.chrome.setBreadcrumbs,
isInfraAvailable: false,
isLogsAvailable: false,
canManagePrivateLocations: false,
}}
>
<Component />
Expand Down
48 changes: 41 additions & 7 deletions x-pack/plugins/observability_solution/synthetics/server/feature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,17 @@ 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 = {
groupType: 'independent' as SubFeaturePrivilegeGroupType,
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: {
Expand All @@ -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,
Expand All @@ -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: {
Expand Down Expand Up @@ -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],
},
],
};
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -168,9 +168,10 @@ export class SyntheticsEsClient {
}

return {
responses: res.body?.responses as unknown as Array<
InferSearchResponseOf<TDocument, TSearchRequest>
>,
responses:
(res?.body?.responses as unknown as Array<
InferSearchResponseOf<TDocument, TSearchRequest>
>) ?? [],
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,27 +305,34 @@ export const syncEditedMonitor = async ({
};

export const validatePermissions = async (
{ server, response, request }: RouteContext,
routeContext: RouteContext,
monitorLocations: MonitorLocations
) => {
const hasPublicLocations = monitorLocations?.some((loc) => loc.isServiceManaged);
if (!hasPublicLocations) {
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: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -38,10 +39,12 @@ export const addPrivateLocationRoute: SyntheticsRestApiRouteFactory<PrivateLocat
body: PrivateLocationSchema,
},
},
requiredPrivileges: [PRIVATE_LOCATION_WRITE_API],
handler: async (routeContext) => {
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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -25,10 +26,13 @@ export const deletePrivateLocationRoute: SyntheticsRestApiRouteFactory<undefined
}),
},
},
requiredPrivileges: [PRIVATE_LOCATION_WRITE_API],
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 { locationId } = request.params as { locationId: string };

const { locations } = await getPrivateLocationsAndAgentPolicies(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading

0 comments on commit 93eedff

Please sign in to comment.