From 05791d4bd18ca877efe96cbcd1aa818a0752a81a Mon Sep 17 00:00:00 2001 From: James Rodewig Date: Tue, 10 Oct 2023 08:03:45 -0500 Subject: [PATCH 1/7] [Serverless Search] Update G/S "Elasticsearch clients" link (#168218) ## Summary **Problem:** The "Elasticsearch Clients" link under **Select your client** points to the HTTP API docs rather than the ES clients docs. **Solution:** Update the link to the point to the **ES Client Libraries** page instead. --- packages/kbn-doc-links/src/get_doc_links.ts | 1 + packages/kbn-doc-links/src/types.ts | 1 + x-pack/plugins/serverless_search/common/doc_links.ts | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index 9784908948f43..181537423fd1d 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -824,6 +824,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { elasticsearch: `${SEARCH_UI_DOCS}tutorials/elasticsearch`, }, serverlessClients: { + clientLib: `${SERVERLESS_ELASTICSEARCH_DOCS}clients`, goApiReference: `${SERVERLESS_ELASTICSEARCH_DOCS}go-client-getting-started`, goGettingStarted: `${SERVERLESS_ELASTICSEARCH_DOCS}go-client-getting-started`, httpApis: `${SERVERLESS_ELASTICSEARCH_DOCS}http-apis`, diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts index ebb4fb07fe21f..6d6d9b7587e51 100644 --- a/packages/kbn-doc-links/src/types.ts +++ b/packages/kbn-doc-links/src/types.ts @@ -581,6 +581,7 @@ export interface DocLinks { readonly elasticsearch: string; }; readonly serverlessClients: { + readonly clientLib: string; readonly goApiReference: string; readonly goGettingStarted: string; readonly httpApis: string; diff --git a/x-pack/plugins/serverless_search/common/doc_links.ts b/x-pack/plugins/serverless_search/common/doc_links.ts index 4c93763a145c8..2ee278b7f3d9c 100644 --- a/x-pack/plugins/serverless_search/common/doc_links.ts +++ b/x-pack/plugins/serverless_search/common/doc_links.ts @@ -61,7 +61,7 @@ class ESDocLinks { this.securityApis = newDocLinks.apis.securityApis; // Client links - this.elasticsearchClients = newDocLinks.serverlessClients.httpApis; + this.elasticsearchClients = newDocLinks.serverlessClients.clientLib; // Go this.goApiReference = newDocLinks.serverlessClients.goApiReference; this.goBasicConfig = newDocLinks.serverlessClients.goGettingStarted; From 348563b52f8ed037f02db0860594c179ec938659 Mon Sep 17 00:00:00 2001 From: Brandon Morelli Date: Tue, 10 Oct 2023 06:31:40 -0700 Subject: [PATCH 2/7] Add security update to 8.10.3 (#168468) --- docs/CHANGELOG.asciidoc | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG.asciidoc b/docs/CHANGELOG.asciidoc index aaceec07701cb..e6e426e7a8623 100644 --- a/docs/CHANGELOG.asciidoc +++ b/docs/CHANGELOG.asciidoc @@ -54,7 +54,19 @@ Review important information about the {kib} 8.x releases. [[release-notes-8.10.3]] == {kib} 8.10.3 -The 8.10.3 release includes the following bug fixes. +[float] +[[security-update-8.10.3]] +=== Security updates + +* **Kibana heap buffer overflow vulnerability** ++ +On Sept 11, 2023, Google Chrome announced CVE-2023-4863, described as “Heap buffer overflow in libwebp in Google Chrome prior to 116.0.5845.187 and libwebp 1.3.2 allowed a remote attacker to perform an out of bounds memory write via a crafted HTML page”. Kibana includes a bundled version of headless Chromium that is only used for Kibana’s reporting capabilities and which is affected by this vulnerability. An exploit for Kibana has not been identified, however as a resolution, the bundled version of Chromium is updated in this release. ++ +The issue is resolved in 8.10.3. ++ +For more information, see our related +https://discuss.elastic.co/t/kibana-8-10-3-7-17-14-security-update/344735[security +announcement]. [float] [[enhancement-v8.10.3]] From 229b883e5052f9fd7ea37c891e6b94e52c87aac1 Mon Sep 17 00:00:00 2001 From: Cristina Amico Date: Tue, 10 Oct 2023 15:33:33 +0200 Subject: [PATCH 3/7] [Fleet] Create template endpoint for integrations inputs (#168015) Closes https://github.com/elastic/kibana/issues/167325 ## Summary Create a new endpoints that returns template integrations inputs with all possible options enabled. It's going to be used for enhancing the standalone flow. This is basically equivalent to the following flow: - Add an integration to a new agent policy - Enable all the options in the package policy - Go to the agent policy page and select action "view agent policy" - Copy only the `inputs` section of it. Actually the api returns the `streams` fields of the inputs and applies a `flatMap`, so the format is slightly different. Note that the api returns the template even when the integration is not installed, since the endpoint retrieves the packageInfo from cache or registry (depending on what it finds). I looked for a way to preserve comments in yaml but it seems that the [js-yaml](https://github.com/nodeca/js-yaml) library does not have this capability. ### Testing Integration that has `inputs.streams` (`yml` and `yaml` options are equivalent): ``` GET kbn:api/fleet/epm/templates/nginx/1.15.0/inputs?format=json GET kbn:api/fleet/epm/templates/nginx/1.15.0/inputs?format=yml GET kbn:api/fleet/epm/templates/nginx/1.15.0/inputs?format=yaml ``` Integration that has only `compiled_inputs`: ``` GET kbn:api/fleet/epm/templates/apm/8.4.2/inputs?format=json GET kbn:api/fleet/epm/templates/apm/8.4.2/inputs?format=yaml ``` ### Checklist - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/fleet/common/constants/routes.ts | 1 + .../plugins/fleet/common/openapi/bundled.json | 50 +++ .../plugins/fleet/common/openapi/bundled.yaml | 31 ++ .../fleet/common/openapi/entrypoint.yaml | 2 + ...lates@{pkg_name}@{pkg_version}@inputs.yaml | 30 ++ .../services/full_agent_policy_to_yaml.ts | 2 +- .../fleet/server/routes/epm/handlers.ts | 24 ++ .../plugins/fleet/server/routes/epm/index.ts | 15 + .../epm/packages/get_template_inputs.ts | 122 +++++++ .../epm/packages/get_templates_inputs.test.ts | 303 ++++++++++++++++++ .../server/services/epm/packages/index.ts | 1 + .../fleet/server/services/package_policy.ts | 13 +- .../fleet/server/types/rest_spec/epm.ts | 12 + .../apis/epm/get_templates_inputs.ts | 200 ++++++++++++ .../fleet_api_integration/apis/epm/index.js | 1 + 15 files changed, 803 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/fleet/common/openapi/paths/epm@templates@{pkg_name}@{pkg_version}@inputs.yaml create mode 100644 x-pack/plugins/fleet/server/services/epm/packages/get_template_inputs.ts create mode 100644 x-pack/plugins/fleet/server/services/epm/packages/get_templates_inputs.test.ts create mode 100644 x-pack/test/fleet_api_integration/apis/epm/get_templates_inputs.ts diff --git a/x-pack/plugins/fleet/common/constants/routes.ts b/x-pack/plugins/fleet/common/constants/routes.ts index d675b1b42bb36..3a5df3768ae96 100644 --- a/x-pack/plugins/fleet/common/constants/routes.ts +++ b/x-pack/plugins/fleet/common/constants/routes.ts @@ -41,6 +41,7 @@ export const EPM_API_ROUTES = { VERIFICATION_KEY_ID: `${EPM_API_ROOT}/verification_key_id`, STATS_PATTERN: `${EPM_PACKAGES_MANY}/{pkgName}/stats`, BULK_ASSETS_PATTERN: `${EPM_API_ROOT}/bulk_assets`, + INPUTS_PATTERN: `${EPM_API_ROOT}/templates/{pkgName}/{pkgVersion}/inputs`, INFO_PATTERN_DEPRECATED: EPM_PACKAGES_ONE_DEPRECATED, INSTALL_FROM_REGISTRY_PATTERN_DEPRECATED: EPM_PACKAGES_ONE_DEPRECATED, diff --git a/x-pack/plugins/fleet/common/openapi/bundled.json b/x-pack/plugins/fleet/common/openapi/bundled.json index a4604a7d7427b..f87ab5a3edacc 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.json +++ b/x-pack/plugins/fleet/common/openapi/bundled.json @@ -1420,6 +1420,56 @@ } ] }, + "/epm/templates/{pkgName}/{pkgVersion}/inputs": { + "get": { + "summary": "Get inputs template", + "tags": [ + "Elastic Package Manager (EPM)" + ], + "responses": { + "400": { + "$ref": "#/components/responses/error" + } + }, + "operationId": "get-inputs-template", + "security": [ + { + "basicAuth": [] + } + ] + }, + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "pkgName", + "in": "path", + "required": true + }, + { + "schema": { + "type": "string" + }, + "name": "pkgVersion", + "in": "path", + "required": true + }, + { + "schema": { + "type": "string", + "enum": [ + "json", + "yaml", + "yml" + ] + }, + "name": "format", + "description": "Format of response - json or yaml", + "in": "query" + } + ] + }, "/agents/setup": { "get": { "summary": "Get agent setup info", diff --git a/x-pack/plugins/fleet/common/openapi/bundled.yaml b/x-pack/plugins/fleet/common/openapi/bundled.yaml index be132c9f19e48..4629a95af27e8 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.yaml +++ b/x-pack/plugins/fleet/common/openapi/bundled.yaml @@ -894,6 +894,37 @@ paths: name: pkgName in: path required: true + /epm/templates/{pkgName}/{pkgVersion}/inputs: + get: + summary: Get inputs template + tags: + - Elastic Package Manager (EPM) + responses: + '400': + $ref: '#/components/responses/error' + operationId: get-inputs-template + security: + - basicAuth: [] + parameters: + - schema: + type: string + name: pkgName + in: path + required: true + - schema: + type: string + name: pkgVersion + in: path + required: true + - schema: + type: string + enum: + - json + - yaml + - yml + name: format + description: Format of response - json or yaml + in: query /agents/setup: get: summary: Get agent setup info diff --git a/x-pack/plugins/fleet/common/openapi/entrypoint.yaml b/x-pack/plugins/fleet/common/openapi/entrypoint.yaml index b8a7e024f3c4e..92bffe4968092 100644 --- a/x-pack/plugins/fleet/common/openapi/entrypoint.yaml +++ b/x-pack/plugins/fleet/common/openapi/entrypoint.yaml @@ -46,6 +46,8 @@ paths: $ref: paths/epm@get_file.yaml '/epm/packages/{pkgName}/stats': $ref: 'paths/epm@packages@{pkg_name}@stats.yaml' + '/epm/templates/{pkgName}/{pkgVersion}/inputs': + $ref: 'paths/epm@templates@{pkg_name}@{pkg_version}@inputs.yaml' # Agent endpoints /agents/setup: diff --git a/x-pack/plugins/fleet/common/openapi/paths/epm@templates@{pkg_name}@{pkg_version}@inputs.yaml b/x-pack/plugins/fleet/common/openapi/paths/epm@templates@{pkg_name}@{pkg_version}@inputs.yaml new file mode 100644 index 0000000000000..9888fd0036ea1 --- /dev/null +++ b/x-pack/plugins/fleet/common/openapi/paths/epm@templates@{pkg_name}@{pkg_version}@inputs.yaml @@ -0,0 +1,30 @@ +get: + summary: Get inputs template + tags: + - Elastic Package Manager (EPM) + responses: + '400': + $ref: ../components/responses/error.yaml + operationId: get-inputs-template + security: + - basicAuth: [] +parameters: + - schema: + type: string + name: pkgName + in: path + required: true + - schema: + type: string + name: pkgVersion + in: path + required: true + - schema: + type: string + enum: + - json + - yaml + - yml + name: format + description: 'Format of response - json or yaml' + in: query diff --git a/x-pack/plugins/fleet/common/services/full_agent_policy_to_yaml.ts b/x-pack/plugins/fleet/common/services/full_agent_policy_to_yaml.ts index 30389ee45cbde..18d995c96f2b8 100644 --- a/x-pack/plugins/fleet/common/services/full_agent_policy_to_yaml.ts +++ b/x-pack/plugins/fleet/common/services/full_agent_policy_to_yaml.ts @@ -39,7 +39,7 @@ export const fullAgentPolicyToYaml = (policy: FullAgentPolicy, toYaml: typeof sa return _formatSecrets(policy.secret_references, yaml); }; -function _sortYamlKeys(keyA: string, keyB: string) { +export function _sortYamlKeys(keyA: string, keyB: string) { const indexA = POLICY_KEYS_ORDER.indexOf(keyA); const indexB = POLICY_KEYS_ORDER.indexOf(keyB); if (indexA >= 0 && indexB < 0) { diff --git a/x-pack/plugins/fleet/server/routes/epm/handlers.ts b/x-pack/plugins/fleet/server/routes/epm/handlers.ts index 5781fa623bb9b..8ce1bf7006f6b 100644 --- a/x-pack/plugins/fleet/server/routes/epm/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/epm/handlers.ts @@ -53,6 +53,7 @@ import type { GetLimitedPackagesRequestSchema, GetBulkAssetsRequestSchema, CreateCustomIntegrationRequestSchema, + GetInputsRequestSchema, } from '../../types'; import { bulkInstallPackages, @@ -67,6 +68,7 @@ import { getLimitedPackages, getInstallation, getBulkAssets, + getTemplateInputs, } from '../../services/epm/packages'; import type { BulkInstallResponse } from '../../services/epm/packages'; import { defaultFleetErrorHandler, fleetErrorToResponseOptions, FleetError } from '../../errors'; @@ -650,6 +652,28 @@ export const reauthorizeTransformsHandler: FleetRequestHandler< } }; +export const getInputsHandler: FleetRequestHandler< + TypeOf, + TypeOf, + undefined +> = async (context, request, response) => { + const soClient = (await context.fleet).internalSoClient; + + try { + const { pkgName, pkgVersion } = request.params; + const { format } = request.query; + let body; + if (format === 'json') { + body = await getTemplateInputs(soClient, pkgName, pkgVersion, 'json'); + } else if (format === 'yml' || format === 'yaml') { + body = await getTemplateInputs(soClient, pkgName, pkgVersion, 'yml'); + } + return response.ok({ body }); + } catch (error) { + return defaultFleetErrorHandler({ error, response }); + } +}; + // Don't expose the whole SO in the API response, only selected fields const soToInstallationInfo = (pkg: PackageListItem | PackageInfo) => { if ('savedObject' in pkg && pkg.savedObject?.attributes) { diff --git a/x-pack/plugins/fleet/server/routes/epm/index.ts b/x-pack/plugins/fleet/server/routes/epm/index.ts index 4f354ae77d7f0..6e0000bf4ccbf 100644 --- a/x-pack/plugins/fleet/server/routes/epm/index.ts +++ b/x-pack/plugins/fleet/server/routes/epm/index.ts @@ -47,6 +47,7 @@ import { ReauthorizeTransformRequestSchema, GetDataStreamsRequestSchema, CreateCustomIntegrationRequestSchema, + GetInputsRequestSchema, } from '../../types'; import { @@ -67,6 +68,7 @@ import { reauthorizeTransformsHandler, getDataStreamsHandler, createCustomIntegrationHandler, + getInputsHandler, } from './handlers'; const MAX_FILE_SIZE_BYTES = 104857600; // 100MB @@ -145,6 +147,19 @@ export const registerRoutes = (router: FleetAuthzRouter) => { getStatsHandler ); + router.versioned + .get({ + path: EPM_API_ROUTES.INPUTS_PATTERN, + fleetAuthz: READ_PACKAGE_INFO_AUTHZ, + }) + .addVersion( + { + version: API_VERSIONS.public.v1, + validate: { request: GetInputsRequestSchema }, + }, + getInputsHandler + ); + router.versioned .get({ path: EPM_API_ROUTES.FILEPATH_PATTERN, diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get_template_inputs.ts b/x-pack/plugins/fleet/server/services/epm/packages/get_template_inputs.ts new file mode 100644 index 0000000000000..04c65535ad0ad --- /dev/null +++ b/x-pack/plugins/fleet/server/services/epm/packages/get_template_inputs.ts @@ -0,0 +1,122 @@ +/* + * 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 type { SavedObjectsClientContract } from '@kbn/core/server'; + +import { merge } from 'lodash'; +import { safeDump } from 'js-yaml'; + +import { packageToPackagePolicy } from '../../../../common/services/package_to_package_policy'; +import { getInputsWithStreamIds, _compilePackagePolicyInputs } from '../../package_policy'; + +import type { + PackageInfo, + NewPackagePolicy, + PackagePolicyInput, + FullAgentPolicyInput, + FullAgentPolicyInputStream, +} from '../../../../common/types'; +import { _sortYamlKeys } from '../../../../common/services/full_agent_policy_to_yaml'; + +import { getPackageInfo } from '.'; + +type Format = 'yml' | 'json'; + +// Function based off storedPackagePolicyToAgentInputs, it only creates the `streams` section instead of the FullAgentPolicyInput +export const templatePackagePolicyToFullInputs = ( + packagePolicyInputs: PackagePolicyInput[] +): FullAgentPolicyInput[] => { + const fullInputs: FullAgentPolicyInput[] = []; + + if (!packagePolicyInputs || packagePolicyInputs.length === 0) return fullInputs; + + packagePolicyInputs.forEach((input) => { + const fullInput = { + ...(input.compiled_input || {}), + ...(input.streams.length + ? { + streams: input.streams.map((stream) => { + const fullStream: FullAgentPolicyInputStream = { + id: stream.id, + type: input.type, + data_stream: stream.data_stream, + ...stream.compiled_stream, + ...Object.entries(stream.config || {}).reduce((acc, [key, { value }]) => { + acc[key] = value; + return acc; + }, {} as { [k: string]: any }), + }; + return fullStream; + }), + } + : {}), + }; + + // deeply merge the input.config values with the full policy input + merge( + fullInput, + Object.entries(input.config || {}).reduce((acc, [key, { value }]) => { + acc[key] = value; + return acc; + }, {} as Record) + ); + fullInputs.push(fullInput); + }); + + return fullInputs; +}; + +export async function getTemplateInputs( + soClient: SavedObjectsClientContract, + pkgName: string, + pkgVersion: string, + format: Format +) { + const packageInfoMap = new Map(); + let packageInfo: PackageInfo; + + if (packageInfoMap.has(pkgName)) { + packageInfo = packageInfoMap.get(pkgName)!; + } else { + packageInfo = await getPackageInfo({ + savedObjectsClient: soClient, + pkgName, + pkgVersion, + }); + } + const emptyPackagePolicy = packageToPackagePolicy(packageInfo, ''); + const inputsWithStreamIds = getInputsWithStreamIds(emptyPackagePolicy, undefined, true); + + const compiledInputs = await _compilePackagePolicyInputs( + packageInfo, + emptyPackagePolicy.vars || {}, + inputsWithStreamIds + ); + const packagePolicyWithInputs: NewPackagePolicy = { + ...emptyPackagePolicy, + inputs: compiledInputs, + }; + const fullAgentPolicyInputs = templatePackagePolicyToFullInputs( + packagePolicyWithInputs.inputs as PackagePolicyInput[] + ); + // @ts-ignore-next-line The return type is any because in some case we can have compiled_input instead of input.streams + // we don't know what it is. An example is integration APM + const inputs: any = fullAgentPolicyInputs.flatMap((input) => input?.streams || input); + + if (format === 'json') { + return { inputs }; + } else if (format === 'yml') { + const yaml = safeDump( + { inputs }, + { + skipInvalid: true, + sortKeys: _sortYamlKeys, + } + ); + return yaml; + } + return { inputs: [] }; +} diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get_templates_inputs.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/get_templates_inputs.test.ts new file mode 100644 index 0000000000000..e9912cc8d8bd2 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/epm/packages/get_templates_inputs.test.ts @@ -0,0 +1,303 @@ +/* + * 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 type { PackagePolicyInput } from '../../../../common/types'; + +import { templatePackagePolicyToFullInputs } from './get_template_inputs'; + +const packageInfoCache = new Map(); +packageInfoCache.set('mock_package-0.0.0', { + name: 'mock_package', + version: '0.0.0', + policy_templates: [ + { + multiple: true, + }, + ], +}); +packageInfoCache.set('limited_package-0.0.0', { + name: 'limited_package', + version: '0.0.0', + policy_templates: [ + { + multiple: false, + }, + ], +}); + +describe('Fleet - templatePackagePolicyToFullInputs', () => { + const mockInput: PackagePolicyInput = { + type: 'test-logs', + enabled: true, + vars: { + inputVar: { value: 'input-value' }, + inputVar2: { value: undefined }, + inputVar3: { + type: 'yaml', + value: 'testField: test', + }, + inputVar4: { value: '' }, + }, + streams: [ + { + id: 'test-logs-foo', + enabled: true, + data_stream: { dataset: 'foo', type: 'logs' }, + vars: { + fooVar: { value: 'foo-value' }, + fooVar2: { value: [1, 2] }, + }, + compiled_stream: { + fooKey: 'fooValue1', + fooKey2: ['fooValue2'], + }, + }, + { + id: 'test-logs-bar', + enabled: true, + data_stream: { dataset: 'bar', type: 'logs' }, + vars: { + barVar: { value: 'bar-value' }, + barVar2: { value: [1, 2] }, + barVar3: { + type: 'yaml', + value: + '- namespace: mockNamespace\n #disabledProp: ["test"]\n anotherProp: test\n- namespace: mockNamespace2\n #disabledProp: ["test2"]\n anotherProp: test2', + }, + barVar4: { + type: 'yaml', + value: '', + }, + barVar5: { + type: 'yaml', + value: 'testField: test\n invalidSpacing: foo', + }, + }, + }, + ], + }; + + const mockInput2: PackagePolicyInput = { + type: 'test-metrics', + policy_template: 'some-template', + enabled: true, + vars: { + inputVar: { value: 'input-value' }, + inputVar2: { value: undefined }, + inputVar3: { + type: 'yaml', + value: 'testField: test', + }, + inputVar4: { value: '' }, + }, + streams: [ + { + id: 'test-metrics-foo', + enabled: true, + data_stream: { dataset: 'foo', type: 'metrics' }, + vars: { + fooVar: { value: 'foo-value' }, + fooVar2: { value: [1, 2] }, + }, + compiled_stream: { + fooKey: 'fooValue1', + fooKey2: ['fooValue2'], + }, + }, + ], + }; + + it('returns no inputs for package policy with no inputs', async () => { + expect(await templatePackagePolicyToFullInputs([])).toEqual([]); + }); + + it('returns inputs even when inputs where disabled', async () => { + expect(await templatePackagePolicyToFullInputs([{ ...mockInput, enabled: false }])).toEqual([ + { + streams: [ + { + data_stream: { + dataset: 'foo', + type: 'logs', + }, + fooKey: 'fooValue1', + fooKey2: ['fooValue2'], + id: 'test-logs-foo', + type: 'test-logs', + }, + { + data_stream: { + dataset: 'bar', + type: 'logs', + }, + id: 'test-logs-bar', + type: 'test-logs', + }, + ], + }, + ]); + }); + + it('returns agent inputs with streams', async () => { + expect(await templatePackagePolicyToFullInputs([mockInput])).toEqual([ + { + streams: [ + { + id: 'test-logs-foo', + data_stream: { dataset: 'foo', type: 'logs' }, + fooKey: 'fooValue1', + fooKey2: ['fooValue2'], + type: 'test-logs', + }, + { + id: 'test-logs-bar', + data_stream: { dataset: 'bar', type: 'logs' }, + type: 'test-logs', + }, + ], + }, + ]); + }); + + it('returns unique agent inputs IDs, with policy template name if one exists for non-limited packages', async () => { + expect(await templatePackagePolicyToFullInputs([mockInput])).toEqual([ + { + streams: [ + { + id: 'test-logs-foo', + type: 'test-logs', + data_stream: { dataset: 'foo', type: 'logs' }, + fooKey: 'fooValue1', + fooKey2: ['fooValue2'], + }, + { + id: 'test-logs-bar', + data_stream: { dataset: 'bar', type: 'logs' }, + type: 'test-logs', + }, + ], + }, + ]); + }); + + it('returns agent inputs without streams', async () => { + expect(await templatePackagePolicyToFullInputs([mockInput2])).toEqual([ + { + streams: [ + { + data_stream: { + dataset: 'foo', + type: 'metrics', + }, + fooKey: 'fooValue1', + fooKey2: ['fooValue2'], + id: 'test-metrics-foo', + type: 'test-metrics', + }, + ], + }, + ]); + }); + + it('returns agent inputs without disabled streams', async () => { + expect( + await templatePackagePolicyToFullInputs([ + { + ...mockInput, + streams: [{ ...mockInput.streams[0] }, { ...mockInput.streams[1], enabled: false }], + }, + ]) + ).toEqual([ + { + streams: [ + { + id: 'test-logs-foo', + type: 'test-logs', + data_stream: { dataset: 'foo', type: 'logs' }, + fooKey: 'fooValue1', + fooKey2: ['fooValue2'], + }, + { + data_stream: { + dataset: 'bar', + type: 'logs', + }, + id: 'test-logs-bar', + type: 'test-logs', + }, + ], + }, + ]); + }); + + it('returns agent inputs with deeply merged config values', async () => { + expect( + await templatePackagePolicyToFullInputs([ + { + ...mockInput, + compiled_input: { + agent_input_template_group1_vars: { + inputVar: 'input-value', + }, + agent_input_template_group2_vars: { + inputVar3: { + testFieldGroup: { + subField1: 'subfield1', + }, + testField: 'test', + }, + }, + }, + config: { + agent_input_template_group1_vars: { + value: { + inputVar2: {}, + }, + }, + agent_input_template_group2_vars: { + value: { + inputVar3: { + testFieldGroup: { + subField2: 'subfield2', + }, + }, + inputVar4: '', + }, + }, + }, + }, + ]) + ).toEqual([ + { + agent_input_template_group1_vars: { + inputVar: 'input-value', + inputVar2: {}, + }, + agent_input_template_group2_vars: { + inputVar3: { + testField: 'test', + testFieldGroup: { + subField1: 'subfield1', + subField2: 'subfield2', + }, + }, + inputVar4: '', + }, + streams: [ + { + id: 'test-logs-foo', + data_stream: { dataset: 'foo', type: 'logs' }, + fooKey: 'fooValue1', + fooKey2: ['fooValue2'], + type: 'test-logs', + }, + { id: 'test-logs-bar', data_stream: { dataset: 'bar', type: 'logs' }, type: 'test-logs' }, + ], + }, + ]); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/index.ts b/x-pack/plugins/fleet/server/services/epm/packages/index.ts index 5895d4a7819c6..f6cee5a34bae0 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/index.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/index.ts @@ -27,6 +27,7 @@ export { export { getBundledPackages } from './bundled_packages'; export { getBulkAssets } from './get_bulk_assets'; +export { getTemplateInputs } from './get_template_inputs'; export type { BulkInstallResponse, IBulkInstallPackageError } from './install'; export { handleInstallPackageFailure, installPackage, ensureInstalledPackage } from './install'; diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 46ac72f69ecab..983fdd7e8d29b 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -1861,16 +1861,23 @@ function validatePackagePolicyOrThrow(packagePolicy: NewPackagePolicy, pkgInfo: } } -function getInputsWithStreamIds( +// the option `allEnabled` is only used to generate inputs integration templates where everything is enabled by default +// it shouldn't be used in the normal install flow +export function getInputsWithStreamIds( packagePolicy: NewPackagePolicy, - packagePolicyId: string + packagePolicyId?: string, + allEnabled?: boolean ): PackagePolicy['inputs'] { return packagePolicy.inputs.map((input) => { return { ...input, + enabled: !!allEnabled ? true : input.enabled, streams: input.streams.map((stream) => ({ ...stream, - id: `${input.type}-${stream.data_stream.dataset}-${packagePolicyId}`, + enabled: !!allEnabled ? true : stream.enabled, + id: packagePolicyId + ? `${input.type}-${stream.data_stream.dataset}-${packagePolicyId}` + : `${input.type}-${stream.data_stream.dataset}`, })), }; }); diff --git a/x-pack/plugins/fleet/server/types/rest_spec/epm.ts b/x-pack/plugins/fleet/server/types/rest_spec/epm.ts index 2c046134598a7..da0628cbeb911 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/epm.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/epm.ts @@ -247,3 +247,15 @@ export const DeletePackageRequestSchemaDeprecated = { }) ), }; + +export const GetInputsRequestSchema = { + params: schema.object({ + pkgName: schema.string(), + pkgVersion: schema.string(), + }), + query: schema.object({ + format: schema.oneOf([schema.literal('json'), schema.literal('yml'), schema.literal('yaml')], { + defaultValue: 'json', + }), + }), +}; diff --git a/x-pack/test/fleet_api_integration/apis/epm/get_templates_inputs.ts b/x-pack/test/fleet_api_integration/apis/epm/get_templates_inputs.ts new file mode 100644 index 0000000000000..e879269c89fa1 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/epm/get_templates_inputs.ts @@ -0,0 +1,200 @@ +/* + * 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 expect from '@kbn/expect'; +import fs from 'fs'; +import path from 'path'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { skipIfNoDockerRegistry } from '../../helpers'; +import { setupFleetAndAgents } from '../agents/services'; +import { testUsers } from '../test_users'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + const testPkgName = 'apache'; + const testPkgVersion = '0.1.4'; + + const uninstallPackage = async (name: string, version: string) => { + await supertest.delete(`/api/fleet/epm/packages/${name}/${version}`).set('kbn-xsrf', 'xxxx'); + }; + + const testPkgArchiveZip = path.join( + path.dirname(__filename), + '../fixtures/direct_upload_packages/apache_0.1.4.zip' + ); + + describe('EPM Templates - Get Inputs', () => { + skipIfNoDockerRegistry(providerContext); + setupFleetAndAgents(providerContext); + before(async () => { + const buf = fs.readFileSync(testPkgArchiveZip); + await supertest + .post(`/api/fleet/epm/packages`) + .set('kbn-xsrf', 'xxxx') + .type('application/zip') + .send(buf) + .expect(200); + }); + after(async () => { + await uninstallPackage(testPkgName, testPkgVersion); + }); + const expectedYml = `inputs: + - id: logfile-apache.access + type: logfile + data_stream: + dataset: apache.access + type: logs + paths: + - /var/log/apache2/access.log* + - /var/log/apache2/other_vhosts_access.log* + - /var/log/httpd/access_log* + exclude_files: + - .gz$ + processors: + - add_fields: + target: '' + fields: + ecs.version: 1.5.0 + - id: logfile-apache.error + type: logfile + data_stream: + dataset: apache.error + type: logs + paths: + - /var/log/apache2/error.log* + - /var/log/httpd/error_log* + exclude_files: + - .gz$ + processors: + - add_locale: null + - add_fields: + target: '' + fields: + ecs.version: 1.5.0 + - id: apache/metrics-apache.status + type: apache/metrics + data_stream: + dataset: apache.status + type: metrics + metricsets: + - status + hosts: + - 'http://127.0.0.1' + period: 10s + server_status_path: /server-status +`; + const expectedJson = [ + { + id: 'logfile-apache.access', + type: 'logfile', + data_stream: { + type: 'logs', + dataset: 'apache.access', + }, + paths: [ + '/var/log/apache2/access.log*', + '/var/log/apache2/other_vhosts_access.log*', + '/var/log/httpd/access_log*', + ], + exclude_files: ['.gz$'], + processors: [ + { + add_fields: { + target: '', + fields: { + 'ecs.version': '1.5.0', + }, + }, + }, + ], + }, + { + id: 'logfile-apache.error', + type: 'logfile', + data_stream: { + type: 'logs', + dataset: 'apache.error', + }, + paths: ['/var/log/apache2/error.log*', '/var/log/httpd/error_log*'], + exclude_files: ['.gz$'], + processors: [ + { + add_locale: null, + }, + { + add_fields: { + target: '', + fields: { + 'ecs.version': '1.5.0', + }, + }, + }, + ], + }, + { + id: 'apache/metrics-apache.status', + type: 'apache/metrics', + data_stream: { + type: 'metrics', + dataset: 'apache.status', + }, + metricsets: ['status'], + hosts: ['http://127.0.0.1'], + period: '10s', + server_status_path: '/server-status', + }, + ]; + + it('returns inputs template in json format', async function () { + const res = await supertest + .get(`/api/fleet/epm/templates/${testPkgName}/${testPkgVersion}/inputs?format=json`) + .expect(200); + const inputs = res.body.inputs; + + expect(inputs).to.eql(expectedJson); + }); + + it('returns inputs template in yaml format if format=yaml', async function () { + const res = await supertest + .get(`/api/fleet/epm/templates/${testPkgName}/${testPkgVersion}/inputs?format=yaml`) + .expect(200); + + expect(res.text).to.eql(expectedYml); + }); + + it('returns inputs template in yaml format if format=yml', async function () { + const res = await supertest + .get(`/api/fleet/epm/templates/${testPkgName}/${testPkgVersion}/inputs?format=yml`) + .expect(200); + expect(res.text).to.eql(expectedYml); + }); + + it('returns a 404 for a version that does not exists', async function () { + await supertest + .get(`/api/fleet/epm/templates/${testPkgName}/0.1.0/inputs?format=json`) + .expect(404); + }); + + it('allows user with only fleet permission to access', async () => { + await supertestWithoutAuth + .get(`/api/fleet/epm/templates/${testPkgName}/${testPkgVersion}/inputs?format=json`) + .auth(testUsers.fleet_all_only.username, testUsers.fleet_all_only.password) + .expect(200); + }); + + it('allows user with integrations read permission to access', async () => { + await supertestWithoutAuth + .get(`/api/fleet/epm/templates/${testPkgName}/${testPkgVersion}/inputs?format=json`) + .auth(testUsers.fleet_all_int_read.username, testUsers.fleet_all_int_read.password) + .expect(200); + }); + }); +} diff --git a/x-pack/test/fleet_api_integration/apis/epm/index.js b/x-pack/test/fleet_api_integration/apis/epm/index.js index 045626e95a740..94f7c12a15ce4 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/index.js +++ b/x-pack/test/fleet_api_integration/apis/epm/index.js @@ -46,5 +46,6 @@ export default function loadTests({ loadTestFile, getService }) { loadTestFile(require.resolve('./install_dynamic_template_metric')); loadTestFile(require.resolve('./routing_rules')); loadTestFile(require.resolve('./install_runtime_field')); + loadTestFile(require.resolve('./get_templates_inputs')); }); } From a0c7d60f8c232213f2bced1f79d14c1e862d42d5 Mon Sep 17 00:00:00 2001 From: James Rodewig Date: Tue, 10 Oct 2023 08:47:15 -0500 Subject: [PATCH 4/7] [Serverless Search] Fix API key privileges link (#168290) ## Summary **Problem:** When creating an API key from the **Getting Started** page, the "Learn how to structure role descriptors" link under **Security Privileges** links to https://www.elastic.co/guide/en/elasticsearch/reference/master/mapping-roles.html. This page talks about Security realms and isn't relevant for Serverless. **Solution:** Update the link to the point to a related page in the Serverless docs instead. **Note:** We can't apply this to the **API Keys** page under **Project settings > Management > API keys** as its shared. --- packages/kbn-doc-links/src/get_doc_links.ts | 3 +++ packages/kbn-doc-links/src/types.ts | 3 +++ x-pack/plugins/serverless_search/common/doc_links.ts | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index 181537423fd1d..074e0f1c921c6 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -848,6 +848,9 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { gettingStartedIngest: `${SERVERLESS_ELASTICSEARCH_DOCS}get-started#ingest`, gettingStartedSearch: `${SERVERLESS_ELASTICSEARCH_DOCS}get-started#search`, }, + serverlessSecurity: { + apiKeyPrivileges: `${SERVERLESS_DOCS}api-keys#restrict-privileges`, + }, synthetics: { featureRoles: `${ELASTIC_WEBSITE_URL}guide/en/observability/${DOC_LINK_VERSION}/synthetics-feature-roles.html`, }, diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts index 6d6d9b7587e51..7a61d9f3dd30e 100644 --- a/packages/kbn-doc-links/src/types.ts +++ b/packages/kbn-doc-links/src/types.ts @@ -605,6 +605,9 @@ export interface DocLinks { readonly integrationsConnectorClient: string; readonly integrationsLogstash: string; }; + readonly serverlessSecurity: { + readonly apiKeyPrivileges: string; + }; readonly synthetics: { readonly featureRoles: string; }; diff --git a/x-pack/plugins/serverless_search/common/doc_links.ts b/x-pack/plugins/serverless_search/common/doc_links.ts index 2ee278b7f3d9c..0c816e3c7a389 100644 --- a/x-pack/plugins/serverless_search/common/doc_links.ts +++ b/x-pack/plugins/serverless_search/common/doc_links.ts @@ -57,7 +57,7 @@ class ESDocLinks { this.kibanaFeedback = newDocLinks.kibana.feedback; this.kibanaRunApiInConsole = newDocLinks.console.serverlessGuide; this.metadata = newDocLinks.security.mappingRoles; - this.roleDescriptors = newDocLinks.security.mappingRoles; + this.roleDescriptors = newDocLinks.serverlessSecurity.apiKeyPrivileges; this.securityApis = newDocLinks.apis.securityApis; // Client links From 3131bc228124e8641dca52508cffd91c14290564 Mon Sep 17 00:00:00 2001 From: Drew Tate Date: Tue, 10 Oct 2023 07:57:52 -0600 Subject: [PATCH 5/7] [Event annotations] expand and stabilize listing page tests (#168139) ## Summary Fix #167434 (reenables embeddable test) Fix https://github.com/elastic/kibana/issues/168281 Flaky test runner (100 times): https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/3417 --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../visualize/group3/_annotation_listing.ts | 35 ++++++++++- .../annotation_listing_page_search.json | 61 +++++++++++++++++++ 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/test/functional/apps/visualize/group3/_annotation_listing.ts b/test/functional/apps/visualize/group3/_annotation_listing.ts index cdb0cb615be47..5d33f714159f0 100644 --- a/test/functional/apps/visualize/group3/_annotation_listing.ts +++ b/test/functional/apps/visualize/group3/_annotation_listing.ts @@ -13,6 +13,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'annotationEditor']); const listingTable = getService('listingTable'); const kibanaServer = getService('kibanaServer'); + const testSubjects = getService('testSubjects'); const find = getService('find'); const retry = getService('retry'); const log = getService('log'); @@ -32,6 +33,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visualize.gotoVisualizationLandingPage(); await PageObjects.visualize.selectAnnotationsTab(); + await listingTable.waitUntilTableIsLoaded(); }); after(async function () { @@ -156,7 +158,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - describe.skip('data view switching', () => { + describe('data view switching', () => { it('recovers from missing data view', async () => { await listingTable.clickItemLink('eventAnnotation', 'missing data view'); @@ -175,7 +177,36 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.annotationEditor.saveGroup(); }); - it('recovers from missing field in data view', () => {}); + it('recovers from missing field in data view', async () => { + const assertShowingMissingFieldError = async (yes: boolean) => { + const [failureExists, canvasExists] = await Promise.all([ + testSubjects.exists('embeddable-lens-failure'), + find.existsByCssSelector('canvas', 1000), + ]); + expect(failureExists).to.be(yes); + expect(canvasExists).to.be(!yes); + }; + + await listingTable.clickItemLink('eventAnnotation', 'Group with additional fields'); + + await assertShowingMissingFieldError(false); + + await retry.try(async () => { + await PageObjects.annotationEditor.editGroupMetadata({ + dataView: 'Data view without fields', + }); + + await assertShowingMissingFieldError(true); + }); + + await retry.try(async () => { + await PageObjects.annotationEditor.editGroupMetadata({ + dataView: 'logs*', + }); + + await assertShowingMissingFieldError(false); + }); + }); }); }); }); diff --git a/test/functional/fixtures/kbn_archiver/annotation_listing_page_search.json b/test/functional/fixtures/kbn_archiver/annotation_listing_page_search.json index 34f4fb8ed1b48..a54a042effb6e 100644 --- a/test/functional/fixtures/kbn_archiver/annotation_listing_page_search.json +++ b/test/functional/fixtures/kbn_archiver/annotation_listing_page_search.json @@ -21,6 +21,29 @@ "version": "WzIyNywxXQ==" } +{ + "attributes": { + "fieldAttrs": "{}", + "fieldFormatMap": "{}", + "fields": "[]", + "name": "Data view without fields", + "runtimeFieldMap": "{}", + "sourceFilters": "[]", + "timeFieldName": "timestamp", + "title": "kibana_sample_data_logs", + "typeMeta": "{}" + }, + "coreMigrationVersion": "8.8.0", + "created_at": "2023-09-07T17:23:20.906Z", + "id": "data-view-without-fields", + "managed": false, + "references": [], + "type": "index-pattern", + "typeMigrationVersion": "8.0.0", + "updated_at": "2023-09-11T15:50:59.227Z", + "version": "WzIyNywxXQ==" +} + { "attributes": { "fieldAttrs": "{}", @@ -44,6 +67,44 @@ "version": "WzIyNywxXQ==" } +{ + "attributes": { + "annotations": [ + { + "extraFields": [ + "@message.raw" + ], + "icon": "triangle", + "id": "3d28ce7e-fc5e-409b-aea3-4d9e15010843", + "key": { + "type": "point_in_time" + }, + "label": "Event", + "timeField": "@timestamp", + "type": "query" + } + ], + "dataViewSpec": null, + "description": "", + "ignoreGlobalFilters": true, + "title": "Group with additional fields" + }, + "coreMigrationVersion": "8.8.0", + "created_at": "2023-10-06T17:15:58.790Z", + "id": "12371e00-5174-11ee-a5c4-7dce2e3293a7", + "managed": false, + "references": [ + { + "id": "90943e30-9a47-11e8-b64d-95841ca0b247", + "name": "event-annotation-group_dataView-ref-90943e30-9a47-11e8-b64d-95841ca0b247", + "type": "index-pattern" + } + ], + "type": "event-annotation-group", + "updated_at": "2023-10-06T17:17:05.384Z", + "version": "WzE4MywxXQ==" +} + { "attributes": { "annotations": [ From 1af7f8c8f4938eb40a0a87f500465e8d413f9c29 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 10 Oct 2023 17:05:55 +0300 Subject: [PATCH 6/7] [Cases] Fix navigation dependency between tests (#168452) --- .../test_suites/observability/cases/configure.ts | 9 +++------ .../test_suites/security/ftr/cases/configure.ts | 5 ++--- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/x-pack/test_serverless/functional/test_suites/observability/cases/configure.ts b/x-pack/test_serverless/functional/test_suites/observability/cases/configure.ts index bd51ed5b848a6..3ed07084edd8c 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/cases/configure.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/cases/configure.ts @@ -10,6 +10,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default ({ getPageObject, getService }: FtrProviderContext) => { const common = getPageObject('common'); + const header = getPageObject('header'); const svlCommonNavigation = getPageObject('svlCommonNavigation'); const svlCommonPage = getPageObject('svlCommonPage'); const svlObltNavigation = getService('svlObltNavigation'); @@ -24,10 +25,10 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { this.tags(['failsOnMKI']); before(async () => { await svlCommonPage.login(); - await svlObltNavigation.navigateToLandingPage(); - await svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'observability-overview:cases' }); + await common.clickAndValidate('configure-case-button', 'case-configure-title'); + await header.waitUntilLoadingHasFinished(); }); after(async () => { @@ -37,10 +38,6 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { // FLAKY: https://github.com/elastic/kibana/issues/166469 describe.skip('Closure options', function () { - before(async () => { - await common.clickAndValidate('configure-case-button', 'case-configure-title'); - }); - it('defaults the closure option correctly', async () => { await cases.common.assertRadioGroupValue('closure-options-radio-group', 'close-by-user'); }); diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/cases/configure.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/configure.ts index e74deac161b26..5c71abf3ad7ba 100644 --- a/x-pack/test_serverless/functional/test_suites/security/ftr/cases/configure.ts +++ b/x-pack/test_serverless/functional/test_suites/security/ftr/cases/configure.ts @@ -10,6 +10,7 @@ import { FtrProviderContext } from '../../../../ftr_provider_context'; export default ({ getPageObject, getService }: FtrProviderContext) => { const common = getPageObject('common'); + const header = getPageObject('header'); const svlCommonPage = getPageObject('svlCommonPage'); const svlSecNavigation = getService('svlSecNavigation'); const testSubjects = getService('testSubjects'); @@ -23,12 +24,10 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { this.tags(['failsOnMKI']); before(async () => { await svlCommonPage.login(); - await svlSecNavigation.navigateToLandingPage(); - await testSubjects.click('solutionSideNavItemLink-cases'); - await common.clickAndValidate('configure-case-button', 'case-configure-title'); + await header.waitUntilLoadingHasFinished(); }); after(async () => { From e3d9f3d62e403f56b9d3a553338a3c5201edc021 Mon Sep 17 00:00:00 2001 From: Sid Date: Tue, 10 Oct 2023 16:12:45 +0200 Subject: [PATCH 7/7] Fix API Key flyout double submit (#167468) ## Summary Closes https://github.com/elastic/kibana/issues/163314 ## Fixes - Removed an extra EuiPortal that was not needed as EuiFlyout adds a Portal - Inverted control of the form by wrapping the flyout content in the form. Prevents multiple submits by using traditional form controls and button type as submit. ## Release Notes: - Fixes issue with multiple API keys being created if the form is submitted using the enter key fired multiple times in quick succession. https://github.com/elastic/kibana/issues/163314 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../public/components/form_flyout.tsx | 104 --- .../api_keys/api_keys_grid/api_key_flyout.tsx | 851 +++++++++--------- 2 files changed, 444 insertions(+), 511 deletions(-) delete mode 100644 x-pack/plugins/security/public/components/form_flyout.tsx diff --git a/x-pack/plugins/security/public/components/form_flyout.tsx b/x-pack/plugins/security/public/components/form_flyout.tsx deleted file mode 100644 index 51ab56a11d225..0000000000000 --- a/x-pack/plugins/security/public/components/form_flyout.tsx +++ /dev/null @@ -1,104 +0,0 @@ -/* - * 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 type { EuiButtonProps, EuiFlyoutProps } from '@elastic/eui'; -import { - EuiButton, - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiFlyoutHeader, - EuiPortal, - EuiTitle, -} from '@elastic/eui'; -import type { FunctionComponent, RefObject } from 'react'; -import React, { useEffect } from 'react'; - -import { FormattedMessage } from '@kbn/i18n-react'; - -import { useHtmlId } from './use_html_id'; - -export interface FormFlyoutProps extends Omit { - title: string; - initialFocus?: RefObject; - onCancel(): void; - onSubmit(): void; - submitButtonText: string; - submitButtonColor?: EuiButtonProps['color']; - isLoading?: EuiButtonProps['isLoading']; - isDisabled?: EuiButtonProps['isDisabled']; - isSubmitButtonHidden?: boolean; -} - -export const FormFlyout: FunctionComponent = ({ - title, - submitButtonText, - submitButtonColor, - onCancel, - onSubmit, - isLoading, - isDisabled, - isSubmitButtonHidden, - children, - initialFocus, - ...rest -}) => { - useEffect(() => { - if (initialFocus && initialFocus.current) { - initialFocus.current.focus(); - } - }, [initialFocus]); - - const titleId = useHtmlId('formFlyout', 'title'); - - return ( - - - - -

{title}

-
-
- {children} - - - - - - - - {!isSubmitButtonHidden && ( - - - {submitButtonText} - - - )} - - -
-
- ); -}; diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_key_flyout.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_key_flyout.tsx index 6d100f2a8261e..b3792941fc6d0 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_key_flyout.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_key_flyout.tsx @@ -7,11 +7,17 @@ import type { ExclusiveUnion } from '@elastic/eui'; import { + EuiButton, + EuiButtonEmpty, EuiCallOut, EuiCheckableCard, EuiFieldNumber, EuiFlexGroup, EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, EuiFormFieldset, EuiFormRow, EuiHorizontalRule, @@ -36,10 +42,9 @@ import { ApiKeyBadge, ApiKeyStatus, TimeToolTip, UsernameWithIcon } from './api_ import type { ApiKeyRoleDescriptors } from '../../../../common/model'; import { DocLink } from '../../../components/doc_link'; import { FormField } from '../../../components/form_field'; -import type { FormFlyoutProps } from '../../../components/form_flyout'; -import { FormFlyout } from '../../../components/form_flyout'; import { FormRow } from '../../../components/form_row'; import { useCurrentUser } from '../../../components/use_current_user'; +import { useHtmlId } from '../../../components/use_html_id'; import { useInitialFocus } from '../../../components/use_initial_focus'; import { RolesAPIClient } from '../../roles/roles_api_client'; import { APIKeysAPIClient } from '../api_keys_api_client'; @@ -64,7 +69,7 @@ export interface ApiKeyFormValues { interface CommonApiKeyFlyoutProps { initialValues?: ApiKeyFormValues; - onCancel: FormFlyoutProps['onCancel']; + onCancel(): void; canManageCrossClusterApiKeys?: boolean; readOnly?: boolean; } @@ -175,337 +180,466 @@ export const ApiKeyFlyout: FunctionComponent = ({ const firstFieldRef = useInitialFocus([isLoading]); + const titleId = useHtmlId('formFlyout', 'title'); + const isSubmitButtonHidden = readOnly || (apiKey && !canEdit); + + const isSubmitDisabled = + isLoading || (formik.submitCount > 0 && !formik.isValid) || readOnly || (apiKey && !canEdit); + + const title = apiKey + ? readOnly || !canEdit + ? i18n.translate('xpack.security.accountManagement.apiKeyFlyout.viewTitle', { + defaultMessage: `API key details`, + }) + : i18n.translate('xpack.security.accountManagement.apiKeyFlyout.updateTitle', { + defaultMessage: `Update API key`, + }) + : i18n.translate('xpack.security.accountManagement.apiKeyFlyout.createTitle', { + defaultMessage: `Create API key`, + }); + + const submitButtonText = apiKey + ? i18n.translate('xpack.security.accountManagement.apiKeyFlyout.updateSubmitButton', { + defaultMessage: `{isSubmitting, select, true{Updating API key…} other{Update API key}}`, + values: { isSubmitting: formik.isSubmitting }, + }) + : i18n.translate('xpack.security.accountManagement.apiKeyFlyout.createSubmitButton', { + defaultMessage: `{isSubmitting, select, true{Creating API key…} other{Create API key}}`, + values: { isSubmitting: formik.isSubmitting }, + }); + return ( - 0 && !formik.isValid) || - readOnly || - (apiKey && !canEdit) - } - isSubmitButtonHidden={readOnly || (apiKey && !canEdit)} - size="m" - ownFocus - > - - {apiKey && !readOnly ? ( - !isOwner ? ( - <> - - } - /> - - - ) : hasExpired ? ( - <> - - } - /> - - - ) : null - ) : null} - -
- - } - fullWidth - > - - - - {apiKey ? ( - <> - - - - - } - > - - - - - - } - > - - - - - + + + +

{title}

+
+
+ + + {apiKey && !readOnly ? ( + !isOwner ? ( + <> + } - > - -
-
- - + + + ) : hasExpired ? ( + <> + } - > - - - -
- - ) : canManageCrossClusterApiKeys ? ( + /> + + + ) : null + ) : null} + } fullWidth > - - - - -

- -

-
- - - - - + formik.setFieldValue('type', 'rest')} - checked={formik.values.type === 'rest'} + ), + }} + fullWidth + /> +
+ + {apiKey ? ( + <> + + + + + } + > + + + + + + } + > + + + + + + } + > + + + + + + } + > + + + + + + ) : canManageCrossClusterApiKeys ? ( + - - - - -

+ } + fullWidth + > + + + + +

+ +

+
+ + -

-
- - - - - - } - onChange={() => formik.setFieldValue('type', 'cross_cluster')} - checked={formik.values.type === 'cross_cluster'} + + + } + onChange={() => formik.setFieldValue('type', 'rest')} + checked={formik.values.type === 'rest'} + /> +
+ + + +

+ +

+
+ + + + + + } + onChange={() => formik.setFieldValue('type', 'cross_cluster')} + checked={formik.values.type === 'cross_cluster'} + /> +
+ +
+ ) : ( + - - - - ) : ( - - } - > - - - )} - + } + > + + + )} + - {formik.values.type === 'cross_cluster' ? ( - - } - helpText={ - + {formik.values.type === 'cross_cluster' ? ( + - - } - fullWidth - > - formik.setFieldValue('access', value)} - validate={(value: string) => { - if (!value) { - return i18n.translate( - 'xpack.security.management.apiKeys.apiKeyFlyout.accessRequired', - { - defaultMessage: 'Enter access permissions or disable this option.', - } - ); + } + helpText={ + + + + } + fullWidth + > + formik.setFieldValue('access', value)} + validate={(value: string) => { + if (!value) { + return i18n.translate( + 'xpack.security.management.apiKeys.apiKeyFlyout.accessRequired', + { + defaultMessage: 'Enter access permissions or disable this option.', + } + ); + } + try { + JSON.parse(value); + } catch (e) { + return i18n.translate( + 'xpack.security.management.apiKeys.apiKeyFlyout.invalidJsonError', + { + defaultMessage: 'Enter valid JSON.', + } + ); + } + }} + fullWidth + languageId="xjson" + height={200} + /> + + ) : ( + + } - try { - JSON.parse(value); - } catch (e) { - return i18n.translate( - 'xpack.security.management.apiKeys.apiKeyFlyout.invalidJsonError', - { - defaultMessage: 'Enter valid JSON.', + checked={formik.values.customPrivileges} + data-test-subj="apiKeysRoleDescriptorsSwitch" + onChange={(e) => formik.setFieldValue('customPrivileges', e.target.checked)} + disabled={readOnly || (apiKey && !canEdit)} + /> + {formik.values.customPrivileges && ( + <> + + + + } - ); - } - }} - fullWidth - languageId="xjson" - height={200} - /> - - ) : ( + fullWidth + data-test-subj="apiKeysRoleDescriptorsCodeEditor" + > + + formik.setFieldValue('role_descriptors', value) + } + validate={(value: string) => { + if (!value) { + return i18n.translate( + 'xpack.security.management.apiKeys.apiKeyFlyout.roleDescriptorsRequired', + { + defaultMessage: 'Enter role descriptors or disable this option.', + } + ); + } + try { + JSON.parse(value); + } catch (e) { + return i18n.translate( + 'xpack.security.management.apiKeys.apiKeyFlyout.invalidJsonError', + { + defaultMessage: 'Enter valid JSON.', + } + ); + } + }} + fullWidth + languageId="xjson" + height={200} + /> + + + + )} + + )} + + {!apiKey && ( + <> + + + + } + checked={formik.values.customExpiration} + onChange={(e) => formik.setFieldValue('customExpiration', e.target.checked)} + disabled={readOnly || !!apiKey} + data-test-subj="apiKeyCustomExpirationSwitch" + /> + {formik.values.customExpiration && ( + <> + + + } + fullWidth + > + + + + + )} + + + )} + } - checked={formik.values.customPrivileges} - data-test-subj="apiKeysRoleDescriptorsSwitch" - onChange={(e) => formik.setFieldValue('customPrivileges', e.target.checked)} + data-test-subj="apiKeysMetadataSwitch" + checked={formik.values.includeMetadata} disabled={readOnly || (apiKey && !canEdit)} + onChange={(e) => formik.setFieldValue('includeMetadata', e.target.checked)} /> - {formik.values.customPrivileges && ( + {formik.values.includeMetadata && ( <> } fullWidth - data-test-subj="apiKeysRoleDescriptorsCodeEditor" > - formik.setFieldValue('role_descriptors', value) - } + value={formik.values.metadata} + onChange={(value: string) => formik.setFieldValue('metadata', value)} validate={(value: string) => { if (!value) { return i18n.translate( - 'xpack.security.management.apiKeys.apiKeyFlyout.roleDescriptorsRequired', + 'xpack.security.management.apiKeys.apiKeyFlyout.metadataRequired', { - defaultMessage: 'Enter role descriptors or disable this option.', + defaultMessage: 'Enter metadata or disable this option.', } ); } @@ -529,137 +663,40 @@ export const ApiKeyFlyout: FunctionComponent = ({ )} - )} - - {!apiKey && ( - <> - - - - } - checked={formik.values.customExpiration} - onChange={(e) => formik.setFieldValue('customExpiration', e.target.checked)} - disabled={readOnly || !!apiKey} - data-test-subj="apiKeyCustomExpirationSwitch" - /> - {formik.values.customExpiration && ( - <> - - - } - fullWidth - > - - - - - )} - - - )} - - - + + + + + - } - data-test-subj="apiKeysMetadataSwitch" - checked={formik.values.includeMetadata} - disabled={readOnly || (apiKey && !canEdit)} - onChange={(e) => formik.setFieldValue('includeMetadata', e.target.checked)} - /> - {formik.values.includeMetadata && ( - <> - - - - - } - fullWidth + + + {!isSubmitButtonHidden && ( + + - formik.setFieldValue('metadata', value)} - validate={(value: string) => { - if (!value) { - return i18n.translate( - 'xpack.security.management.apiKeys.apiKeyFlyout.metadataRequired', - { - defaultMessage: 'Enter metadata or disable this option.', - } - ); - } - try { - JSON.parse(value); - } catch (e) { - return i18n.translate( - 'xpack.security.management.apiKeys.apiKeyFlyout.invalidJsonError', - { - defaultMessage: 'Enter valid JSON.', - } - ); - } - }} - fullWidth - languageId="xjson" - height={200} - /> - - - + {submitButtonText} + + )} - - -
-
+ + + +
); };