mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
# Backport This will backport the following commits from `main` to `8.x`: - [[Synthetics] Fixes partial updates for params and params viewing (#195866)](https://github.com/elastic/kibana/pull/195866) <!--- Backport version: 9.4.3 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Shahzad","email":"shahzad31comp@gmail.com"},"sourceCommit":{"committedDate":"2024-10-25T15:06:52Z","message":"[Synthetics] Fixes partial updates for params and params viewing (#195866)\n\n## Summary\r\n\r\nFixes https://github.com/elastic/kibana/issues/167781\r\n\r\nIn docs we says that only key/value pairs are required, but in actual\r\nedit, that means rest of the data was being lost on edits\r\n\r\nAllow partial updates to params edit API !!\r\n\r\nThis PR makes sure prev objects is fetched and merged with new data\r\nhence allowing partial updates !!\r\n\r\nWe are also allowing the ability to view value of the secret once it's\r\nsaved via API !!\r\n\r\n### Value is hidden\r\nParam value will not be visible unless user is `super_user` or\r\n`kibana_admin`, though user can assign new value.\r\n\r\n---------\r\n\r\nCo-authored-by: Justin Kambic <jk@elastic.co>","sha":"0ff9a8a9d9ff2bdb99562eeca29152bd0a0c4385","branchLabelMapping":{"^v9.0.0$":"main","^v8.17.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","backport:prev-minor","ci:project-deploy-observability","Team:obs-ux-management"],"title":"[Synthetics] Fixes partial updates for params and params viewing","number":195866,"url":"https://github.com/elastic/kibana/pull/195866","mergeCommit":{"message":"[Synthetics] Fixes partial updates for params and params viewing (#195866)\n\n## Summary\r\n\r\nFixes https://github.com/elastic/kibana/issues/167781\r\n\r\nIn docs we says that only key/value pairs are required, but in actual\r\nedit, that means rest of the data was being lost on edits\r\n\r\nAllow partial updates to params edit API !!\r\n\r\nThis PR makes sure prev objects is fetched and merged with new data\r\nhence allowing partial updates !!\r\n\r\nWe are also allowing the ability to view value of the secret once it's\r\nsaved via API !!\r\n\r\n### Value is hidden\r\nParam value will not be visible unless user is `super_user` or\r\n`kibana_admin`, though user can assign new value.\r\n\r\n---------\r\n\r\nCo-authored-by: Justin Kambic <jk@elastic.co>","sha":"0ff9a8a9d9ff2bdb99562eeca29152bd0a0c4385"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/195866","number":195866,"mergeCommit":{"message":"[Synthetics] Fixes partial updates for params and params viewing (#195866)\n\n## Summary\r\n\r\nFixes https://github.com/elastic/kibana/issues/167781\r\n\r\nIn docs we says that only key/value pairs are required, but in actual\r\nedit, that means rest of the data was being lost on edits\r\n\r\nAllow partial updates to params edit API !!\r\n\r\nThis PR makes sure prev objects is fetched and merged with new data\r\nhence allowing partial updates !!\r\n\r\nWe are also allowing the ability to view value of the secret once it's\r\nsaved via API !!\r\n\r\n### Value is hidden\r\nParam value will not be visible unless user is `super_user` or\r\n`kibana_admin`, though user can assign new value.\r\n\r\n---------\r\n\r\nCo-authored-by: Justin Kambic <jk@elastic.co>","sha":"0ff9a8a9d9ff2bdb99562eeca29152bd0a0c4385"}}]}] BACKPORT--> Co-authored-by: Shahzad <shahzad31comp@gmail.com>
This commit is contained in:
parent
9f5ff8321a
commit
4c22558e57
12 changed files with 357 additions and 112 deletions
|
@ -26,13 +26,13 @@ You must have `all` privileges for the *Synthetics* feature in the *{observabili
|
|||
[[parameter-edit-request-body]]
|
||||
==== Request body
|
||||
|
||||
The request body should contain the following attributes:
|
||||
The request body can contain the following attributes, it can't be empty at least one attribute is required.:
|
||||
|
||||
`key`::
|
||||
(Required, string) The key of the parameter.
|
||||
(Optional, string) The key of the parameter.
|
||||
|
||||
`value`::
|
||||
(Required, string) The updated value associated with the parameter.
|
||||
(Optional, string) The updated value associated with the parameter.
|
||||
|
||||
`description`::
|
||||
(Optional, string) The updated description of the parameter.
|
||||
|
|
|
@ -76,8 +76,8 @@ journey(`GlobalParameters`, async ({ page, params }) => {
|
|||
await page.click('text=Delete ParameterEdit Parameter >> :nth-match(button, 2)');
|
||||
await page.click('[aria-label="Key"]');
|
||||
await page.fill('[aria-label="Key"]', 'username2');
|
||||
await page.click('[aria-label="Value"]');
|
||||
await page.fill('[aria-label="Value"]', 'elastic2');
|
||||
await page.click('[aria-label="New value"]');
|
||||
await page.fill('[aria-label="New value"]', 'elastic2');
|
||||
await page.click('.euiComboBox__inputWrap');
|
||||
await page.fill('[aria-label="Tags"]', 'staging');
|
||||
await page.press('[aria-label="Tags"]', 'Enter');
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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 { EuiText } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
|
||||
export function OptionalText() {
|
||||
return (
|
||||
<EuiText size="xs" color="subdued">
|
||||
{i18n.translate('xpack.synthetics.sloEdit.optionalLabel', {
|
||||
defaultMessage: 'Optional',
|
||||
})}
|
||||
</EuiText>
|
||||
);
|
||||
}
|
|
@ -22,6 +22,7 @@ import { FormProvider } from 'react-hook-form';
|
|||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { NoPermissionsTooltip } from '../../common/components/permissions';
|
||||
import {
|
||||
addNewGlobalParamAction,
|
||||
|
@ -80,18 +81,29 @@ export const AddParamFlyout = ({
|
|||
const onSubmit = (formData: SyntheticsParams) => {
|
||||
const { namespaces, ...paramRequest } = formData;
|
||||
const shareAcrossSpaces = namespaces?.includes(ALL_SPACES_ID);
|
||||
const newParamData = {
|
||||
...paramRequest,
|
||||
};
|
||||
|
||||
if (isEditingItem && id) {
|
||||
// omit value if it's empty
|
||||
if (isEmpty(newParamData.value)) {
|
||||
// @ts-ignore this is a valid check
|
||||
delete newParamData.value;
|
||||
}
|
||||
}
|
||||
|
||||
if (isEditingItem && id) {
|
||||
dispatch(
|
||||
editGlobalParamAction.get({
|
||||
id,
|
||||
paramRequest: { ...paramRequest, share_across_spaces: shareAcrossSpaces },
|
||||
paramRequest,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
dispatch(
|
||||
addNewGlobalParamAction.get({
|
||||
...paramRequest,
|
||||
...newParamData,
|
||||
share_across_spaces: shareAcrossSpaces,
|
||||
})
|
||||
);
|
||||
|
|
|
@ -6,16 +6,11 @@
|
|||
*/
|
||||
import React from 'react';
|
||||
import { ALL_SPACES_ID } from '@kbn/security-plugin/public';
|
||||
import {
|
||||
EuiCheckbox,
|
||||
EuiComboBox,
|
||||
EuiFieldText,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiTextArea,
|
||||
} from '@elastic/eui';
|
||||
import { EuiCheckbox, EuiComboBox, EuiFieldText, EuiForm, EuiFormRow } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Controller, useFormContext, useFormState } from 'react-hook-form';
|
||||
import { OptionalText } from '../components/optional_text';
|
||||
import { ParamValueField } from './param_value_field';
|
||||
import { SyntheticsParams } from '../../../../../../common/runtime_types';
|
||||
import { ListParamItem } from './params_list';
|
||||
|
||||
|
@ -61,25 +56,8 @@ export const AddParamForm = ({
|
|||
})}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={VALUE_LABEL}
|
||||
isInvalid={Boolean(errors?.value)}
|
||||
error={errors?.value?.message}
|
||||
>
|
||||
<EuiTextArea
|
||||
data-test-subj="syntheticsAddParamFormTextArea"
|
||||
fullWidth
|
||||
aria-label={VALUE_LABEL}
|
||||
{...register('value', {
|
||||
required: {
|
||||
value: true,
|
||||
message: VALUE_REQUIRED,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow fullWidth label={TAGS_LABEL}>
|
||||
<ParamValueField isEditingItem={isEditingItem} />
|
||||
<EuiFormRow fullWidth label={TAGS_LABEL} labelAppend={<OptionalText />}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="tags"
|
||||
|
@ -102,7 +80,7 @@ export const AddParamForm = ({
|
|||
)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow fullWidth label={DESCRIPTION_LABEL}>
|
||||
<EuiFormRow fullWidth label={DESCRIPTION_LABEL} labelAppend={<OptionalText />}>
|
||||
<EuiFieldText
|
||||
data-test-subj="syntheticsAddParamFormFieldText"
|
||||
fullWidth
|
||||
|
@ -173,6 +151,6 @@ const KEY_EXISTS = i18n.translate('xpack.synthetics.monitorManagement.param.keyE
|
|||
defaultMessage: 'Key already exists',
|
||||
});
|
||||
|
||||
const VALUE_REQUIRED = i18n.translate('xpack.synthetics.monitorManagement.value.required', {
|
||||
export const VALUE_REQUIRED = i18n.translate('xpack.synthetics.monitorManagement.value.required', {
|
||||
defaultMessage: 'Value is required',
|
||||
});
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* 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 { EuiCallOut, EuiFormRow, EuiSpacer, EuiTextArea } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { useFormContext, useFormState } from 'react-hook-form';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { OptionalText } from '../components/optional_text';
|
||||
import { ListParamItem } from './params_list';
|
||||
import { SyntheticsParams } from '../../../../../../common/runtime_types';
|
||||
import { VALUE_LABEL, VALUE_REQUIRED } from './add_param_form';
|
||||
|
||||
export const ParamValueField = ({ isEditingItem }: { isEditingItem: ListParamItem | null }) => {
|
||||
const { register } = useFormContext<SyntheticsParams>();
|
||||
const { errors } = useFormState<SyntheticsParams>();
|
||||
|
||||
if (isEditingItem) {
|
||||
return (
|
||||
<>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={NEW_VALUE_LABEL}
|
||||
isInvalid={Boolean(errors?.value)}
|
||||
error={errors?.value?.message}
|
||||
labelAppend={<OptionalText />}
|
||||
>
|
||||
<EuiTextArea
|
||||
data-test-subj="syntheticsAddParamFormTextArea"
|
||||
fullWidth
|
||||
aria-label={NEW_VALUE_LABEL}
|
||||
{...register('value')}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiCallOut
|
||||
size="s"
|
||||
title={i18n.translate('xpack.synthetics.paramValueField.euiCallOut.newValue', {
|
||||
defaultMessage:
|
||||
'Assign a new value to update this parameter, or leave blank to keep the current value.',
|
||||
})}
|
||||
iconType="iInCircle"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={VALUE_LABEL}
|
||||
isInvalid={Boolean(errors?.value)}
|
||||
error={errors?.value?.message}
|
||||
>
|
||||
<EuiTextArea
|
||||
data-test-subj="syntheticsAddParamFormTextArea"
|
||||
fullWidth
|
||||
aria-label={VALUE_LABEL}
|
||||
{...register('value', {
|
||||
required: {
|
||||
value: true,
|
||||
message: VALUE_REQUIRED,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
||||
|
||||
export const NEW_VALUE_LABEL = i18n.translate(
|
||||
'xpack.synthetics.monitorManagement.paramForm.newValue',
|
||||
{
|
||||
defaultMessage: 'New value',
|
||||
}
|
||||
);
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { isEmpty } from 'lodash';
|
||||
import { INITIAL_REST_VERSION, SYNTHETICS_API_URLS } from '../../../../../common/constants';
|
||||
import {
|
||||
DeleteParamsResponse,
|
||||
|
@ -35,16 +36,22 @@ export const editGlobalParam = async ({
|
|||
id,
|
||||
}: {
|
||||
id: string;
|
||||
paramRequest: SyntheticsParamRequest;
|
||||
}): Promise<SyntheticsParams> =>
|
||||
apiService.put<SyntheticsParams>(
|
||||
paramRequest: Partial<SyntheticsParamRequest>;
|
||||
}): Promise<SyntheticsParams> => {
|
||||
const data = paramRequest;
|
||||
if (isEmpty(paramRequest.value)) {
|
||||
// omit empty value
|
||||
delete data.value;
|
||||
}
|
||||
return await apiService.put<SyntheticsParams>(
|
||||
SYNTHETICS_API_URLS.PARAMS + `/${id}`,
|
||||
paramRequest,
|
||||
data,
|
||||
SyntheticsParamsCodec,
|
||||
{
|
||||
version: INITIAL_REST_VERSION,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const deleteGlobalParams = async (ids: string[]): Promise<DeleteParamsResponse[]> =>
|
||||
apiService.delete(
|
||||
|
|
|
@ -9,6 +9,7 @@ import { schema, TypeOf } from '@kbn/config-schema';
|
|||
import { SavedObjectsFindResponse } from '@kbn/core/server';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { escapeQuotes } from '@kbn/es-query';
|
||||
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
|
||||
import { RouteContext } from './types';
|
||||
import { MonitorSortFieldSchema } from '../../common/runtime_types/monitor_management/sort_field';
|
||||
import { getAllLocations } from '../synthetics_service/get_all_locations';
|
||||
|
@ -269,3 +270,26 @@ function parseMappingKey(key: string | undefined) {
|
|||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
export const validateRouteSpaceName = async (routeContext: RouteContext) => {
|
||||
const { spaceId, server, request, response } = routeContext;
|
||||
if (spaceId === DEFAULT_SPACE_ID) {
|
||||
// default space is always valid
|
||||
return { spaceId: DEFAULT_SPACE_ID };
|
||||
}
|
||||
|
||||
try {
|
||||
await server.spaces?.spacesService.getActiveSpace(request);
|
||||
} catch (error) {
|
||||
if (error.output?.statusCode === 404) {
|
||||
return {
|
||||
spaceId,
|
||||
invalidResponse: response.notFound({
|
||||
body: { message: `Kibana space '${spaceId}' does not exist` },
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { invalidResponse: undefined };
|
||||
};
|
||||
|
|
|
@ -19,8 +19,12 @@ import { syntheticsParamType } from '../../../../common/types/saved_objects';
|
|||
import { SYNTHETICS_API_URLS } from '../../../../common/constants';
|
||||
|
||||
const ParamsObjectSchema = schema.object({
|
||||
key: schema.string(),
|
||||
value: schema.string(),
|
||||
key: schema.string({
|
||||
minLength: 1,
|
||||
}),
|
||||
value: schema.string({
|
||||
minLength: 1,
|
||||
}),
|
||||
description: schema.maybe(schema.string()),
|
||||
tags: schema.maybe(schema.arrayOf(schema.string())),
|
||||
share_across_spaces: schema.maybe(schema.boolean()),
|
||||
|
|
|
@ -6,8 +6,9 @@
|
|||
*/
|
||||
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { SavedObject } from '@kbn/core/server';
|
||||
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
|
||||
import { SavedObject, SavedObjectsErrorHelpers } from '@kbn/core/server';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { validateRouteSpaceName } from '../../common';
|
||||
import { SyntheticsRestApiRouteFactory } from '../../types';
|
||||
import { SyntheticsParamRequest, SyntheticsParams } from '../../../../common/runtime_types';
|
||||
import { syntheticsParamType } from '../../../../common/types/saved_objects';
|
||||
|
@ -20,7 +21,7 @@ const RequestParamsSchema = schema.object({
|
|||
type RequestParams = TypeOf<typeof RequestParamsSchema>;
|
||||
|
||||
export const editSyntheticsParamsRoute: SyntheticsRestApiRouteFactory<
|
||||
SyntheticsParams,
|
||||
SyntheticsParams | undefined,
|
||||
RequestParams
|
||||
> = () => ({
|
||||
method: 'PUT',
|
||||
|
@ -30,46 +31,63 @@ export const editSyntheticsParamsRoute: SyntheticsRestApiRouteFactory<
|
|||
request: {
|
||||
params: RequestParamsSchema,
|
||||
body: schema.object({
|
||||
key: schema.string(),
|
||||
value: schema.string(),
|
||||
key: schema.maybe(
|
||||
schema.string({
|
||||
minLength: 1,
|
||||
})
|
||||
),
|
||||
value: schema.maybe(
|
||||
schema.string({
|
||||
minLength: 1,
|
||||
})
|
||||
),
|
||||
description: schema.maybe(schema.string()),
|
||||
tags: schema.maybe(schema.arrayOf(schema.string())),
|
||||
share_across_spaces: schema.maybe(schema.boolean()),
|
||||
}),
|
||||
},
|
||||
},
|
||||
handler: async ({ savedObjectsClient, request, server, response }) => {
|
||||
try {
|
||||
const { id: _spaceId } = (await server.spaces?.spacesService.getActiveSpace(request)) ?? {
|
||||
id: DEFAULT_SPACE_ID,
|
||||
};
|
||||
const { id } = request.params;
|
||||
const { share_across_spaces: _shareAcrossSpaces, ...data } =
|
||||
request.body as SyntheticsParamRequest & {
|
||||
id: string;
|
||||
};
|
||||
handler: async (routeContext) => {
|
||||
const { savedObjectsClient, request, response, spaceId, server } = routeContext;
|
||||
const { invalidResponse } = await validateRouteSpaceName(routeContext);
|
||||
if (invalidResponse) return invalidResponse;
|
||||
|
||||
const { value } = data;
|
||||
const { id: paramId } = request.params;
|
||||
const data = request.body as SyntheticsParamRequest;
|
||||
if (isEmpty(data)) {
|
||||
return response.badRequest({ body: { message: 'Request body cannot be empty' } });
|
||||
}
|
||||
const encryptedSavedObjectsClient = server.encryptedSavedObjects.getClient();
|
||||
|
||||
try {
|
||||
const existingParam =
|
||||
await encryptedSavedObjectsClient.getDecryptedAsInternalUser<SyntheticsParams>(
|
||||
syntheticsParamType,
|
||||
paramId,
|
||||
{ namespace: spaceId }
|
||||
);
|
||||
|
||||
const newParam = {
|
||||
...existingParam.attributes,
|
||||
...data,
|
||||
};
|
||||
|
||||
// value from data since we aren't using encrypted client
|
||||
const { value } = existingParam.attributes;
|
||||
const {
|
||||
id: responseId,
|
||||
attributes: { key, tags, description },
|
||||
namespaces,
|
||||
} = (await savedObjectsClient.update(
|
||||
} = (await savedObjectsClient.update<SyntheticsParams>(
|
||||
syntheticsParamType,
|
||||
id,
|
||||
data
|
||||
paramId,
|
||||
newParam
|
||||
)) as SavedObject<SyntheticsParams>;
|
||||
|
||||
return { id: responseId, key, tags, description, namespaces, value };
|
||||
} 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` },
|
||||
});
|
||||
} catch (getErr) {
|
||||
if (SavedObjectsErrorHelpers.isNotFoundError(getErr)) {
|
||||
return response.notFound({ body: { message: 'Param not found' } });
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { SavedObject, SavedObjectsFindResult } from '@kbn/core-saved-objects-api-server';
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { SyntheticsRestApiRouteFactory } from '../../types';
|
||||
import { RouteContext, SyntheticsRestApiRouteFactory } from '../../types';
|
||||
import { syntheticsParamType } from '../../../../common/types/saved_objects';
|
||||
import { SYNTHETICS_API_URLS } from '../../../../common/constants';
|
||||
import { SyntheticsParams, SyntheticsParamsReadonly } from '../../../../common/runtime_types';
|
||||
|
@ -30,45 +30,13 @@ export const getSyntheticsParamsRoute: SyntheticsRestApiRouteFactory<
|
|||
params: RequestParamsSchema,
|
||||
},
|
||||
},
|
||||
handler: async ({ savedObjectsClient, request, response, server, spaceId }) => {
|
||||
handler: async (routeContext) => {
|
||||
const { savedObjectsClient, request, response, spaceId } = routeContext;
|
||||
try {
|
||||
const { id: paramId } = request.params;
|
||||
|
||||
const encryptedSavedObjectsClient = server.encryptedSavedObjects.getClient();
|
||||
|
||||
const canSave =
|
||||
(
|
||||
await server.coreStart?.capabilities.resolveCapabilities(request, {
|
||||
capabilityPath: 'uptime.*',
|
||||
})
|
||||
).uptime.save ?? false;
|
||||
|
||||
if (canSave) {
|
||||
if (paramId) {
|
||||
const savedObject =
|
||||
await encryptedSavedObjectsClient.getDecryptedAsInternalUser<SyntheticsParams>(
|
||||
syntheticsParamType,
|
||||
paramId,
|
||||
{ namespace: spaceId }
|
||||
);
|
||||
return toClientResponse(savedObject);
|
||||
}
|
||||
|
||||
const finder =
|
||||
await encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser<SyntheticsParams>(
|
||||
{
|
||||
type: syntheticsParamType,
|
||||
perPage: 1000,
|
||||
namespaces: [spaceId],
|
||||
}
|
||||
);
|
||||
|
||||
const hits: Array<SavedObjectsFindResult<SyntheticsParams>> = [];
|
||||
for await (const result of finder.find()) {
|
||||
hits.push(...result.saved_objects);
|
||||
}
|
||||
|
||||
return hits.map((savedObject) => toClientResponse(savedObject));
|
||||
if (await isAnAdminUser(routeContext)) {
|
||||
return getDecryptedParams(routeContext, paramId);
|
||||
} else {
|
||||
if (paramId) {
|
||||
const savedObject = await savedObjectsClient.get<SyntheticsParamsReadonly>(
|
||||
|
@ -78,11 +46,7 @@ export const getSyntheticsParamsRoute: SyntheticsRestApiRouteFactory<
|
|||
return toClientResponse(savedObject);
|
||||
}
|
||||
|
||||
const data = await savedObjectsClient.find<SyntheticsParamsReadonly>({
|
||||
type: syntheticsParamType,
|
||||
perPage: 10000,
|
||||
});
|
||||
return data.saved_objects.map((savedObject) => toClientResponse(savedObject));
|
||||
return findAllParams(routeContext);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.output?.statusCode === 404) {
|
||||
|
@ -94,6 +58,70 @@ export const getSyntheticsParamsRoute: SyntheticsRestApiRouteFactory<
|
|||
},
|
||||
});
|
||||
|
||||
const isAnAdminUser = async (routeContext: RouteContext) => {
|
||||
const { request, server } = routeContext;
|
||||
const user = server.coreStart.security.authc.getCurrentUser(request);
|
||||
|
||||
const isSuperUser = user?.roles.includes('superuser');
|
||||
const isAdmin = user?.roles.includes('kibana_admin');
|
||||
|
||||
const canSave =
|
||||
(
|
||||
await server.coreStart?.capabilities.resolveCapabilities(request, {
|
||||
capabilityPath: 'uptime.*',
|
||||
})
|
||||
).uptime.save ?? false;
|
||||
|
||||
return (isSuperUser || isAdmin) && canSave;
|
||||
};
|
||||
|
||||
const getDecryptedParams = async ({ server, spaceId }: RouteContext, paramId?: string) => {
|
||||
const encryptedSavedObjectsClient = server.encryptedSavedObjects.getClient();
|
||||
|
||||
if (paramId) {
|
||||
const savedObject =
|
||||
await encryptedSavedObjectsClient.getDecryptedAsInternalUser<SyntheticsParams>(
|
||||
syntheticsParamType,
|
||||
paramId,
|
||||
{ namespace: spaceId }
|
||||
);
|
||||
return toClientResponse(savedObject);
|
||||
}
|
||||
const finder =
|
||||
await encryptedSavedObjectsClient.createPointInTimeFinderDecryptedAsInternalUser<SyntheticsParams>(
|
||||
{
|
||||
type: syntheticsParamType,
|
||||
perPage: 1000,
|
||||
namespaces: [spaceId],
|
||||
}
|
||||
);
|
||||
|
||||
const hits: Array<SavedObjectsFindResult<SyntheticsParams>> = [];
|
||||
for await (const result of finder.find()) {
|
||||
hits.push(...result.saved_objects);
|
||||
}
|
||||
|
||||
void finder.close();
|
||||
|
||||
return hits.map((savedObject) => toClientResponse(savedObject));
|
||||
};
|
||||
|
||||
const findAllParams = async ({ savedObjectsClient }: RouteContext) => {
|
||||
const finder = savedObjectsClient.createPointInTimeFinder<SyntheticsParams>({
|
||||
type: syntheticsParamType,
|
||||
perPage: 1000,
|
||||
});
|
||||
|
||||
const hits: Array<SavedObjectsFindResult<SyntheticsParams>> = [];
|
||||
for await (const result of finder.find()) {
|
||||
hits.push(...result.saved_objects);
|
||||
}
|
||||
|
||||
void finder.close();
|
||||
|
||||
return hits.map((savedObject) => toClientResponse(savedObject));
|
||||
};
|
||||
|
||||
const toClientResponse = (
|
||||
savedObject: SavedObject<SyntheticsParams | SyntheticsParamsReadonly>
|
||||
) => {
|
||||
|
|
|
@ -10,6 +10,7 @@ import { pick } from 'lodash';
|
|||
import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants';
|
||||
import expect from '@kbn/expect';
|
||||
import { syntheticsParamType } from '@kbn/synthetics-plugin/common/types/saved_objects';
|
||||
import { SyntheticsMonitorTestService } from './services/synthetics_monitor_test_service';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
import { PrivateLocationTestService } from './services/private_location_test_service';
|
||||
|
||||
|
@ -21,12 +22,15 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
describe('AddEditParams', function () {
|
||||
this.tags('skipCloud');
|
||||
const supertestAPI = getService('supertest');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
|
||||
const kServer = getService('kibanaServer');
|
||||
const testParam = {
|
||||
key: 'test',
|
||||
value: 'test',
|
||||
};
|
||||
const testPrivateLocations = new PrivateLocationTestService(getService);
|
||||
const monitorTestService = new SyntheticsMonitorTestService(getService);
|
||||
|
||||
before(async () => {
|
||||
await testPrivateLocations.installSyntheticsPackage();
|
||||
|
@ -93,6 +97,12 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
const param = getResponse.body[0];
|
||||
assertHas(param, testParam);
|
||||
|
||||
await supertestAPI
|
||||
.put(SYNTHETICS_API_URLS.PARAMS + '/' + param.id)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({})
|
||||
.expect(400);
|
||||
|
||||
await supertestAPI
|
||||
.put(SYNTHETICS_API_URLS.PARAMS + '/' + param.id)
|
||||
.set('kbn-xsrf', 'true')
|
||||
|
@ -107,6 +117,55 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
assertHas(actualUpdatedParam, expectedUpdatedParam);
|
||||
});
|
||||
|
||||
it('handles partial editing a param', async () => {
|
||||
const newParam = {
|
||||
key: 'testUpdated',
|
||||
value: 'testUpdated',
|
||||
tags: ['a tag'],
|
||||
description: 'test description',
|
||||
};
|
||||
|
||||
const response = await supertestAPI
|
||||
.post(SYNTHETICS_API_URLS.PARAMS)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send(newParam)
|
||||
.expect(200);
|
||||
const paramId = response.body.id;
|
||||
|
||||
const getResponse = await supertestAPI
|
||||
.get(SYNTHETICS_API_URLS.PARAMS + '/' + paramId)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.expect(200);
|
||||
assertHas(getResponse.body, newParam);
|
||||
|
||||
await supertestAPI
|
||||
.put(SYNTHETICS_API_URLS.PARAMS + '/' + paramId)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
key: 'testUpdated',
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
await supertestAPI
|
||||
.put(SYNTHETICS_API_URLS.PARAMS + '/' + paramId)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
key: 'testUpdatedAgain',
|
||||
value: 'testUpdatedAgain',
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const updatedGetResponse = await supertestAPI
|
||||
.get(SYNTHETICS_API_URLS.PARAMS + '/' + paramId)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.expect(200);
|
||||
assertHas(updatedGetResponse.body, {
|
||||
...newParam,
|
||||
key: 'testUpdatedAgain',
|
||||
value: 'testUpdatedAgain',
|
||||
});
|
||||
});
|
||||
|
||||
it('handles spaces', async () => {
|
||||
const SPACE_ID = `test-space-${uuidv4()}`;
|
||||
const SPACE_NAME = `test-space-name ${uuidv4()}`;
|
||||
|
@ -277,5 +336,22 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
expect(getResponse.body[0].namespaces).eql(['*']);
|
||||
assertHas(getResponse.body[0], testParam);
|
||||
});
|
||||
|
||||
it('should not return values for non admin user', async () => {
|
||||
const { username, password } = await monitorTestService.addsNewSpace();
|
||||
const resp = await supertestWithoutAuth
|
||||
.get(`${SYNTHETICS_API_URLS.PARAMS}`)
|
||||
.auth(username, password)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send()
|
||||
.expect(200);
|
||||
|
||||
const params = resp.body;
|
||||
expect(params.length).to.eql(6);
|
||||
params.forEach((param: any) => {
|
||||
expect(param.value).to.eql(undefined);
|
||||
expect(param.key).to.not.empty();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue