Skip to content

Commit

Permalink
[Synthetics] only allow params to be saved against specific namespaces (
Browse files Browse the repository at this point in the history
#155759)

## Summary

This PR prevents saving synthetics params to arbitrary Kibana spaces.

Previously, we accepted a `namespaces` array which could be used to save
a param to spaces that do not exist. In the past, the client would send
this value as `namespaces: ['*']` when a param is meant to e shared
across spaces.

This PR replaces the `namespaces` array with a simple
`share_across_spaces` boolean.

Params are now only allowed to be saved to the current active space, or
`*`.

---------

Co-authored-by: shahzad31 <shahzad31comp@gmail.com>
  • Loading branch information
dominiqueclarke and shahzad31 authored Apr 26, 2023
1 parent 5a74646 commit ead18ac
Show file tree
Hide file tree
Showing 13 changed files with 407 additions and 60 deletions.
2 changes: 2 additions & 0 deletions x-pack/plugins/security/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export type {

export type { AuthenticationServiceStart, AuthenticationServiceSetup } from './authentication';

export { ALL_SPACES_ID } from '../common/constants';

export const plugin: PluginInitializer<
SecurityPluginSetup,
SecurityPluginStart,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import * as t from 'io-ts';

export const SyntheticsParamCode = t.intersection([
export const SyntheticsParamSOCodec = t.intersection([
t.interface({
key: t.string,
value: t.string,
Expand All @@ -19,4 +19,18 @@ export const SyntheticsParamCode = t.intersection([
}),
]);

export type SyntheticsParam = t.TypeOf<typeof SyntheticsParamCode>;
export type SyntheticsParamSO = t.TypeOf<typeof SyntheticsParamSOCodec>;

export const SyntheticsParamRequestCodec = t.intersection([
t.interface({
key: t.string,
value: t.string,
}),
t.partial({
description: t.string,
tags: t.array(t.string),
share_across_spaces: t.boolean,
}),
]);

export type SyntheticsParamRequest = t.TypeOf<typeof SyntheticsParamRequestCodec>;
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* 2.0.
*/
import React, { useCallback, useEffect, useState } from 'react';
import { ALL_SPACES_ID } from '@kbn/security-plugin/public';
import {
EuiFlyout,
EuiFlyoutBody,
Expand All @@ -25,7 +26,7 @@ import { useDispatch } from 'react-redux';
import { apiService } from '../../../../../utils/api_service';
import { ClientPluginsStart } from '../../../../../plugin';
import { ListParamItem } from './params_list';
import { SyntheticsParam } from '../../../../../../common/runtime_types';
import { SyntheticsParamSO } from '../../../../../../common/runtime_types';
import { useFormWrapped } from '../../../../../hooks/use_form_wrapped';
import { AddParamForm } from './add_param_form';
import { SYNTHETICS_API_URLS } from '../../../../../../common/constants';
Expand All @@ -46,7 +47,7 @@ export const AddParamFlyout = ({

const { id, ...dataToSave } = isEditingItem ?? {};

const form = useFormWrapped<SyntheticsParam>({
const form = useFormWrapped<SyntheticsParamSO>({
mode: 'onSubmit',
reValidateMode: 'onChange',
shouldFocusError: true,
Expand All @@ -66,23 +67,32 @@ export const AddParamFlyout = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [setIsEditingItem]);

const [paramData, setParamData] = useState<SyntheticsParam | null>(null);
const [paramData, setParamData] = useState<SyntheticsParamSO | null>(null);

const { application } = useKibana<ClientPluginsStart>().services;

const { loading, data } = useFetcher(async () => {
if (!paramData) {
return;
}
const { namespaces, ...paramRequest } = paramData;
const shareAcrossSpaces = namespaces?.includes(ALL_SPACES_ID);
if (isEditingItem) {
return apiService.put(SYNTHETICS_API_URLS.PARAMS, { id, ...paramData });
return apiService.put(SYNTHETICS_API_URLS.PARAMS, {
id,
...paramRequest,
share_across_spaces: shareAcrossSpaces,
});
}
return apiService.post(SYNTHETICS_API_URLS.PARAMS, paramData);
return apiService.post(SYNTHETICS_API_URLS.PARAMS, {
...paramRequest,
share_across_spaces: shareAcrossSpaces,
});
}, [paramData]);

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

const onSubmit = (formData: SyntheticsParam) => {
const onSubmit = (formData: SyntheticsParamSO) => {
setParamData(formData);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* 2.0.
*/
import React from 'react';
import { ALL_SPACES_ID } from '@kbn/security-plugin/public';
import {
EuiCheckbox,
EuiComboBox,
Expand All @@ -15,7 +16,7 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { Controller, useFormContext, useFormState } from 'react-hook-form';
import { SyntheticsParam } from '../../../../../../common/runtime_types';
import { SyntheticsParamSO } from '../../../../../../common/runtime_types';
import { ListParamItem } from './params_list';

export const AddParamForm = ({
Expand All @@ -25,8 +26,8 @@ export const AddParamForm = ({
items: ListParamItem[];
isEditingItem: ListParamItem | null;
}) => {
const { register, control } = useFormContext<SyntheticsParam>();
const { errors } = useFormState<SyntheticsParam>();
const { register, control } = useFormContext<SyntheticsParamSO>();
const { errors } = useFormState<SyntheticsParamSO>();

const tagsList = items.reduce((acc, item) => {
const tags = item.tags || [];
Expand Down Expand Up @@ -119,7 +120,7 @@ export const AddParamForm = ({
aria-label={NAMESPACES_LABEL}
onChange={(e) => {
if (e.target.checked) {
field.onChange(['*']);
field.onChange([ALL_SPACES_ID]);
} else {
field.onChange([]);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ import { EuiBasicTableColumn } from '@elastic/eui/src/components/basic_table/bas
import { useDebounce } from 'react-use';
import { TableTitle } from '../../common/components/table_title';
import { ParamsText } from './params_text';
import { SyntheticsParam } from '../../../../../../common/runtime_types';
import { SyntheticsParamSO } from '../../../../../../common/runtime_types';
import { useParamsList } from '../hooks/use_params_list';
import { AddParamFlyout } from './add_param_flyout';
import { DeleteParam } from './delete_param';

export interface ListParamItem extends SyntheticsParam {
export interface ListParamItem extends SyntheticsParamSO {
id: string;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
import { useFetcher } from '@kbn/observability-plugin/public';
import { SavedObject } from '@kbn/core-saved-objects-common';
import { useMemo } from 'react';
import { SyntheticsParam } from '../../../../../../common/runtime_types';
import { SyntheticsParamSO } from '../../../../../../common/runtime_types';
import { apiService } from '../../../../../utils/api_service';
import { SYNTHETICS_API_URLS } from '../../../../../../common/constants';

export const useParamsList = (lastRefresh: number) => {
const { data, loading } = useFetcher<
Promise<{ data: Array<SavedObject<SyntheticsParam>> }>
Promise<{ data: Array<SavedObject<SyntheticsParamSO>> }>
>(() => {
return apiService.get(SYNTHETICS_API_URLS.PARAMS);
}, [lastRefresh]);
Expand Down
31 changes: 21 additions & 10 deletions x-pack/plugins/synthetics/server/routes/settings/add_param.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,45 @@
*/

import { schema } from '@kbn/config-schema';
import { ALL_SPACES_ID } from '@kbn/security-plugin/common/constants';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
import { SyntheticsParam } from '../../../common/runtime_types';
import { SyntheticsParamRequest, SyntheticsParamSO } from '../../../common/runtime_types';
import { syntheticsParamType } from '../../../common/types/saved_objects';
import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types';
import { SYNTHETICS_API_URLS } from '../../../common/constants';

export const addSyntheticsParamsRoute: SyntheticsRestApiRouteFactory = () => ({
method: 'POST',
path: SYNTHETICS_API_URLS.PARAMS,

validate: {
body: schema.object({
key: schema.string(),
value: schema.string(),
description: schema.maybe(schema.string()),
tags: schema.maybe(schema.arrayOf(schema.string())),
namespaces: schema.maybe(schema.arrayOf(schema.string())),
share_across_spaces: schema.maybe(schema.boolean()),
}),
},
writeAccess: true,
handler: async ({ request, server, savedObjectsClient }): Promise<any> => {
const { namespaces, ...data } = request.body as SyntheticsParam;
const spaceId = server.spaces?.spacesService.getSpaceId(request) ?? DEFAULT_SPACE_ID;
handler: async ({ request, response, server, savedObjectsClient }): Promise<any> => {
try {
const { id: spaceId } = (await server.spaces?.spacesService.getActiveSpace(request)) ?? {
id: DEFAULT_SPACE_ID,
};
const { share_across_spaces: shareAcrossSpaces, ...data } =
request.body as SyntheticsParamRequest;

const result = await savedObjectsClient.create(syntheticsParamType, data, {
initialNamespaces: (namespaces ?? []).length > 0 ? namespaces : [spaceId],
});
const result = await savedObjectsClient.create<SyntheticsParamSO>(syntheticsParamType, data, {
initialNamespaces: shareAcrossSpaces ? [ALL_SPACES_ID] : [spaceId],
});
return { data: result };
} catch (error) {
if (error.output?.statusCode === 404) {
const spaceId = server.spaces?.spacesService.getSpaceId(request) ?? DEFAULT_SPACE_ID;
return response.notFound({ body: { message: `Kibana space '${spaceId}' does not exist` } });
}

return { data: result };
throw error;
}
},
});
31 changes: 25 additions & 6 deletions x-pack/plugins/synthetics/server/routes/settings/edit_param.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
*/

import { schema } from '@kbn/config-schema';
import { SyntheticsParam } from '../../../common/runtime_types';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
import { SyntheticsParamRequest } from '../../../common/runtime_types';
import { syntheticsParamType } from '../../../common/types/saved_objects';
import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types';
import { SYNTHETICS_API_URLS } from '../../../common/constants';
Expand All @@ -21,15 +22,33 @@ export const editSyntheticsParamsRoute: SyntheticsRestApiRouteFactory = () => ({
value: schema.string(),
description: schema.maybe(schema.string()),
tags: schema.maybe(schema.arrayOf(schema.string())),
namespaces: schema.maybe(schema.arrayOf(schema.string())),
share_across_spaces: schema.maybe(schema.boolean()),
}),
},
writeAccess: true,
handler: async ({ savedObjectsClient, request, server }): Promise<any> => {
const { namespaces, id, ...data } = request.body as SyntheticsParam & { id: string };
handler: async ({ savedObjectsClient, request, response, server }): Promise<any> => {
try {
const { id: _spaceId } = (await server.spaces?.spacesService.getActiveSpace(request)) ?? {
id: DEFAULT_SPACE_ID,
};
const {
share_across_spaces: shareAcrossSpaces,
id,
...data
} = request.body as SyntheticsParamRequest & {
id: string;
};

const result = await savedObjectsClient.update(syntheticsParamType, id, data);
const result = await savedObjectsClient.update(syntheticsParamType, id, data);

return { data: result };
return { data: result };
} catch (error) {
if (error.output?.statusCode === 404) {
const spaceId = server.spaces?.spacesService.getSpaceId(request) ?? DEFAULT_SPACE_ID;
return response.notFound({ body: { message: `Kibana space '${spaceId}' does not exist` } });
}

throw error;
}
},
});
59 changes: 35 additions & 24 deletions x-pack/plugins/synthetics/server/routes/settings/params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,35 +15,46 @@ export const getSyntheticsParamsRoute: SyntheticsRestApiRouteFactory = () => ({
method: 'GET',
path: SYNTHETICS_API_URLS.PARAMS,
validate: {},
handler: async ({ savedObjectsClient, request, server }): Promise<any> => {
const encryptedSavedObjectsClient = server.encryptedSavedObjects.getClient();

const spaceId = server.spaces?.spacesService.getSpaceId(request) ?? DEFAULT_SPACE_ID;

const canSave =
(await server.coreStart?.capabilities.resolveCapabilities(request)).uptime.save ?? false;

if (canSave) {
const finder =
await encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser({
handler: async ({ savedObjectsClient, request, response, server }): Promise<any> => {
try {
const encryptedSavedObjectsClient = server.encryptedSavedObjects.getClient();

const { id: spaceId } = (await server.spaces?.spacesService.getActiveSpace(request)) ?? {
id: DEFAULT_SPACE_ID,
};

const canSave =
(await server.coreStart?.capabilities.resolveCapabilities(request)).uptime.save ?? false;

if (canSave) {
const finder =
await encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser({
type: syntheticsParamType,
perPage: 1000,
namespaces: [spaceId],
});

const hits: SavedObjectsFindResult[] = [];
for await (const result of finder.find()) {
hits.push(...result.saved_objects);
}

return { data: hits };
} else {
const data = await savedObjectsClient.find({
type: syntheticsParamType,
perPage: 1000,
namespaces: [spaceId],
perPage: 10000,
});

const hits: SavedObjectsFindResult[] = [];
for await (const result of finder.find()) {
hits.push(...result.saved_objects);
return { data: data.saved_objects };
}
} catch (error) {
if (error.output?.statusCode === 404) {
const spaceId = server.spaces?.spacesService.getSpaceId(request) ?? DEFAULT_SPACE_ID;
return response.notFound({ body: { message: `Kibana space '${spaceId}' does not exist` } });
}

return { data: hits };
} else {
const data = await savedObjectsClient.find({
type: syntheticsParamType,
perPage: 10000,
});

return { data: data.saved_objects };
throw error;
}
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import {
ServiceLocations,
SyntheticsMonitorWithId,
SyntheticsMonitorWithSecrets,
SyntheticsParam,
SyntheticsParamSO,
ThrottlingOptions,
} from '../../common/runtime_types';
import { getServiceLocations } from './get_service_locations';
Expand Down Expand Up @@ -558,7 +558,7 @@ export class SyntheticsService {
const paramsBySpace: Record<string, Record<string, string>> = Object.create(null);

const finder =
await encryptedClient.createPointInTimeFinderDecryptedAsInternalUser<SyntheticsParam>({
await encryptedClient.createPointInTimeFinderDecryptedAsInternalUser<SyntheticsParamSO>({
type: syntheticsParamType,
perPage: 1000,
namespaces: spaceId ? [spaceId] : undefined,
Expand Down
Loading

0 comments on commit ead18ac

Please sign in to comment.