diff --git a/oas_docs/bundle.json b/oas_docs/bundle.json index 04f9e908bc70..fe29203663ca 100644 --- a/oas_docs/bundle.json +++ b/oas_docs/bundle.json @@ -44288,339 +44288,6 @@ ] } }, - "/api/fleet/remote_synced_integrations/status": { - "get": { - "description": "[Required authorization] Route required privileges: fleet-settings-read AND integrations-read.", - "operationId": "get-fleet-remote-synced-integrations-status", - "parameters": [], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "additionalProperties": false, - "properties": { - "custom_assets": { - "additionalProperties": { - "additionalProperties": false, - "properties": { - "error": { - "type": "string" - }, - "is_deleted": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "package_name": { - "type": "string" - }, - "package_version": { - "type": "string" - }, - "sync_status": { - "enum": [ - "completed", - "synchronizing", - "failed", - "warning" - ], - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "type", - "name", - "package_name", - "package_version", - "sync_status" - ], - "type": "object" - }, - "type": "object" - }, - "error": { - "type": "string" - }, - "integrations": { - "items": { - "additionalProperties": false, - "properties": { - "error": { - "type": "string" - }, - "id": { - "type": "string" - }, - "install_status": { - "additionalProperties": false, - "properties": { - "main": { - "type": "string" - }, - "remote": { - "type": "string" - } - }, - "required": [ - "main" - ], - "type": "object" - }, - "package_name": { - "type": "string" - }, - "package_version": { - "type": "string" - }, - "sync_status": { - "enum": [ - "completed", - "synchronizing", - "failed", - "warning" - ], - "type": "string" - }, - "updated_at": { - "type": "string" - }, - "warning": { - "type": "string" - } - }, - "required": [ - "sync_status", - "install_status" - ], - "type": "object" - }, - "type": "array" - }, - "warning": { - "type": "string" - } - }, - "required": [ - "integrations" - ], - "type": "object" - } - } - } - }, - "400": { - "content": { - "application/json": { - "schema": { - "additionalProperties": false, - "description": "Generic Error", - "properties": { - "attributes": {}, - "error": { - "type": "string" - }, - "errorType": { - "type": "string" - }, - "message": { - "type": "string" - }, - "statusCode": { - "type": "number" - } - }, - "required": [ - "message", - "attributes" - ], - "type": "object" - } - } - } - } - }, - "summary": "Get CCR Remote synced integrations status", - "tags": [ - "CCR Remote synced integrations" - ] - } - }, - "/api/fleet/remote_synced_integrations/{outputId}/remote_status": { - "get": { - "description": "[Required authorization] Route required privileges: fleet-settings-read AND integrations-read.", - "operationId": "get-fleet-remote-synced-integrations-outputid-remote-status", - "parameters": [ - { - "in": "path", - "name": "outputId", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "additionalProperties": false, - "properties": { - "custom_assets": { - "additionalProperties": { - "additionalProperties": false, - "properties": { - "error": { - "type": "string" - }, - "is_deleted": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "package_name": { - "type": "string" - }, - "package_version": { - "type": "string" - }, - "sync_status": { - "enum": [ - "completed", - "synchronizing", - "failed", - "warning" - ], - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "type", - "name", - "package_name", - "package_version", - "sync_status" - ], - "type": "object" - }, - "type": "object" - }, - "error": { - "type": "string" - }, - "integrations": { - "items": { - "additionalProperties": false, - "properties": { - "error": { - "type": "string" - }, - "id": { - "type": "string" - }, - "install_status": { - "additionalProperties": false, - "properties": { - "main": { - "type": "string" - }, - "remote": { - "type": "string" - } - }, - "required": [ - "main" - ], - "type": "object" - }, - "package_name": { - "type": "string" - }, - "package_version": { - "type": "string" - }, - "sync_status": { - "enum": [ - "completed", - "synchronizing", - "failed", - "warning" - ], - "type": "string" - }, - "updated_at": { - "type": "string" - }, - "warning": { - "type": "string" - } - }, - "required": [ - "sync_status", - "install_status" - ], - "type": "object" - }, - "type": "array" - }, - "warning": { - "type": "string" - } - }, - "required": [ - "integrations" - ], - "type": "object" - } - } - } - }, - "400": { - "content": { - "application/json": { - "schema": { - "additionalProperties": false, - "description": "Generic Error", - "properties": { - "attributes": {}, - "error": { - "type": "string" - }, - "errorType": { - "type": "string" - }, - "message": { - "type": "string" - }, - "statusCode": { - "type": "number" - } - }, - "required": [ - "message", - "attributes" - ], - "type": "object" - } - } - } - } - }, - "summary": "Get CCR Remote synced integrations status by outputId", - "tags": [ - "CCR Remote synced integrations" - ] - } - }, "/api/fleet/service_tokens": { "post": { "description": "[Required authorization] Route required privileges: fleet-agents-all.", @@ -58703,9 +58370,6 @@ { "name": "alerting" }, - { - "name": "CCR Remote synced integrations" - }, { "name": "connectors" }, diff --git a/oas_docs/bundle.serverless.json b/oas_docs/bundle.serverless.json index 3ad4d2da3600..de956f9e37af 100644 --- a/oas_docs/bundle.serverless.json +++ b/oas_docs/bundle.serverless.json @@ -44288,339 +44288,6 @@ ] } }, - "/api/fleet/remote_synced_integrations/status": { - "get": { - "description": "[Required authorization] Route required privileges: fleet-settings-read AND integrations-read.", - "operationId": "get-fleet-remote-synced-integrations-status", - "parameters": [], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "additionalProperties": false, - "properties": { - "custom_assets": { - "additionalProperties": { - "additionalProperties": false, - "properties": { - "error": { - "type": "string" - }, - "is_deleted": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "package_name": { - "type": "string" - }, - "package_version": { - "type": "string" - }, - "sync_status": { - "enum": [ - "completed", - "synchronizing", - "failed", - "warning" - ], - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "type", - "name", - "package_name", - "package_version", - "sync_status" - ], - "type": "object" - }, - "type": "object" - }, - "error": { - "type": "string" - }, - "integrations": { - "items": { - "additionalProperties": false, - "properties": { - "error": { - "type": "string" - }, - "id": { - "type": "string" - }, - "install_status": { - "additionalProperties": false, - "properties": { - "main": { - "type": "string" - }, - "remote": { - "type": "string" - } - }, - "required": [ - "main" - ], - "type": "object" - }, - "package_name": { - "type": "string" - }, - "package_version": { - "type": "string" - }, - "sync_status": { - "enum": [ - "completed", - "synchronizing", - "failed", - "warning" - ], - "type": "string" - }, - "updated_at": { - "type": "string" - }, - "warning": { - "type": "string" - } - }, - "required": [ - "sync_status", - "install_status" - ], - "type": "object" - }, - "type": "array" - }, - "warning": { - "type": "string" - } - }, - "required": [ - "integrations" - ], - "type": "object" - } - } - } - }, - "400": { - "content": { - "application/json": { - "schema": { - "additionalProperties": false, - "description": "Generic Error", - "properties": { - "attributes": {}, - "error": { - "type": "string" - }, - "errorType": { - "type": "string" - }, - "message": { - "type": "string" - }, - "statusCode": { - "type": "number" - } - }, - "required": [ - "message", - "attributes" - ], - "type": "object" - } - } - } - } - }, - "summary": "Get CCR Remote synced integrations status", - "tags": [ - "CCR Remote synced integrations" - ] - } - }, - "/api/fleet/remote_synced_integrations/{outputId}/remote_status": { - "get": { - "description": "[Required authorization] Route required privileges: fleet-settings-read AND integrations-read.", - "operationId": "get-fleet-remote-synced-integrations-outputid-remote-status", - "parameters": [ - { - "in": "path", - "name": "outputId", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "additionalProperties": false, - "properties": { - "custom_assets": { - "additionalProperties": { - "additionalProperties": false, - "properties": { - "error": { - "type": "string" - }, - "is_deleted": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "package_name": { - "type": "string" - }, - "package_version": { - "type": "string" - }, - "sync_status": { - "enum": [ - "completed", - "synchronizing", - "failed", - "warning" - ], - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "type", - "name", - "package_name", - "package_version", - "sync_status" - ], - "type": "object" - }, - "type": "object" - }, - "error": { - "type": "string" - }, - "integrations": { - "items": { - "additionalProperties": false, - "properties": { - "error": { - "type": "string" - }, - "id": { - "type": "string" - }, - "install_status": { - "additionalProperties": false, - "properties": { - "main": { - "type": "string" - }, - "remote": { - "type": "string" - } - }, - "required": [ - "main" - ], - "type": "object" - }, - "package_name": { - "type": "string" - }, - "package_version": { - "type": "string" - }, - "sync_status": { - "enum": [ - "completed", - "synchronizing", - "failed", - "warning" - ], - "type": "string" - }, - "updated_at": { - "type": "string" - }, - "warning": { - "type": "string" - } - }, - "required": [ - "sync_status", - "install_status" - ], - "type": "object" - }, - "type": "array" - }, - "warning": { - "type": "string" - } - }, - "required": [ - "integrations" - ], - "type": "object" - } - } - } - }, - "400": { - "content": { - "application/json": { - "schema": { - "additionalProperties": false, - "description": "Generic Error", - "properties": { - "attributes": {}, - "error": { - "type": "string" - }, - "errorType": { - "type": "string" - }, - "message": { - "type": "string" - }, - "statusCode": { - "type": "number" - } - }, - "required": [ - "message", - "attributes" - ], - "type": "object" - } - } - } - } - }, - "summary": "Get CCR Remote synced integrations status by outputId", - "tags": [ - "CCR Remote synced integrations" - ] - } - }, "/api/fleet/service_tokens": { "post": { "description": "[Required authorization] Route required privileges: fleet-agents-all.", @@ -58294,9 +57961,6 @@ { "name": "alerting" }, - { - "name": "CCR Remote synced integrations" - }, { "name": "connectors" }, diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index 6b789a7da422..38940dd7baa3 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -66,7 +66,6 @@ tags: Configure APM source maps. A source map allows minified files to be mapped back to original source code--allowing you to maintain the speed advantage of minified code, without losing the ability to quickly and easily debug your application. For best results, uploading source maps should become a part of your deployment procedure, and not something you only do when you see unhelpful errors. That's because uploading source maps after errors happen won't make old errors magically readable--errors must occur again for source mapping to occur. name: APM sourcemaps - - name: CCR Remote synced integrations - name: connectors description: | Connectors provide a central place to store connection information for services and integrations with Elastic or third party systems. Alerting rules can use connectors to run actions when rule conditions are met. @@ -39060,233 +39059,6 @@ paths: summary: Update a proxy tags: - Fleet proxies - /api/fleet/remote_synced_integrations/{outputId}/remote_status: - get: - description: '[Required authorization] Route required privileges: fleet-settings-read AND integrations-read.' - operationId: get-fleet-remote-synced-integrations-outputid-remote-status - parameters: - - in: path - name: outputId - required: true - schema: - type: string - responses: - '200': - content: - application/json: - schema: - additionalProperties: false - type: object - properties: - custom_assets: - additionalProperties: - additionalProperties: false - type: object - properties: - error: - type: string - is_deleted: - type: boolean - name: - type: string - package_name: - type: string - package_version: - type: string - sync_status: - enum: - - completed - - synchronizing - - failed - - warning - type: string - type: - type: string - required: - - type - - name - - package_name - - package_version - - sync_status - type: object - error: - type: string - integrations: - items: - additionalProperties: false - type: object - properties: - error: - type: string - id: - type: string - install_status: - additionalProperties: false - type: object - properties: - main: - type: string - remote: - type: string - required: - - main - package_name: - type: string - package_version: - type: string - sync_status: - enum: - - completed - - synchronizing - - failed - - warning - type: string - updated_at: - type: string - warning: - type: string - required: - - sync_status - - install_status - type: array - warning: - type: string - required: - - integrations - '400': - content: - application/json: - schema: - additionalProperties: false - description: Generic Error - type: object - properties: - attributes: {} - error: - type: string - errorType: - type: string - message: - type: string - statusCode: - type: number - required: - - message - - attributes - summary: Get CCR Remote synced integrations status by outputId - tags: - - CCR Remote synced integrations - /api/fleet/remote_synced_integrations/status: - get: - description: '[Required authorization] Route required privileges: fleet-settings-read AND integrations-read.' - operationId: get-fleet-remote-synced-integrations-status - parameters: [] - responses: - '200': - content: - application/json: - schema: - additionalProperties: false - type: object - properties: - custom_assets: - additionalProperties: - additionalProperties: false - type: object - properties: - error: - type: string - is_deleted: - type: boolean - name: - type: string - package_name: - type: string - package_version: - type: string - sync_status: - enum: - - completed - - synchronizing - - failed - - warning - type: string - type: - type: string - required: - - type - - name - - package_name - - package_version - - sync_status - type: object - error: - type: string - integrations: - items: - additionalProperties: false - type: object - properties: - error: - type: string - id: - type: string - install_status: - additionalProperties: false - type: object - properties: - main: - type: string - remote: - type: string - required: - - main - package_name: - type: string - package_version: - type: string - sync_status: - enum: - - completed - - synchronizing - - failed - - warning - type: string - updated_at: - type: string - warning: - type: string - required: - - sync_status - - install_status - type: array - warning: - type: string - required: - - integrations - '400': - content: - application/json: - schema: - additionalProperties: false - description: Generic Error - type: object - properties: - attributes: {} - error: - type: string - errorType: - type: string - message: - type: string - statusCode: - type: number - required: - - message - - attributes - summary: Get CCR Remote synced integrations status - tags: - - CCR Remote synced integrations /api/fleet/service_tokens: post: description: '[Required authorization] Route required privileges: fleet-agents-all.' diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index ee33a5b88d33..7adf845f370e 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -80,7 +80,6 @@ tags: description: Cases documentation url: https://www.elastic.co/docs/explore-analyze/alerts-cases/cases x-displayName: Cases - - name: CCR Remote synced integrations - name: connectors description: | Connectors provide a central place to store connection information for services and integrations with Elastic or third party systems. Alerting rules can use connectors to run actions when rule conditions are met. @@ -41302,233 +41301,6 @@ paths: summary: Update a proxy tags: - Fleet proxies - /api/fleet/remote_synced_integrations/{outputId}/remote_status: - get: - description: '[Required authorization] Route required privileges: fleet-settings-read AND integrations-read.' - operationId: get-fleet-remote-synced-integrations-outputid-remote-status - parameters: - - in: path - name: outputId - required: true - schema: - type: string - responses: - '200': - content: - application/json: - schema: - additionalProperties: false - type: object - properties: - custom_assets: - additionalProperties: - additionalProperties: false - type: object - properties: - error: - type: string - is_deleted: - type: boolean - name: - type: string - package_name: - type: string - package_version: - type: string - sync_status: - enum: - - completed - - synchronizing - - failed - - warning - type: string - type: - type: string - required: - - type - - name - - package_name - - package_version - - sync_status - type: object - error: - type: string - integrations: - items: - additionalProperties: false - type: object - properties: - error: - type: string - id: - type: string - install_status: - additionalProperties: false - type: object - properties: - main: - type: string - remote: - type: string - required: - - main - package_name: - type: string - package_version: - type: string - sync_status: - enum: - - completed - - synchronizing - - failed - - warning - type: string - updated_at: - type: string - warning: - type: string - required: - - sync_status - - install_status - type: array - warning: - type: string - required: - - integrations - '400': - content: - application/json: - schema: - additionalProperties: false - description: Generic Error - type: object - properties: - attributes: {} - error: - type: string - errorType: - type: string - message: - type: string - statusCode: - type: number - required: - - message - - attributes - summary: Get CCR Remote synced integrations status by outputId - tags: - - CCR Remote synced integrations - /api/fleet/remote_synced_integrations/status: - get: - description: '[Required authorization] Route required privileges: fleet-settings-read AND integrations-read.' - operationId: get-fleet-remote-synced-integrations-status - parameters: [] - responses: - '200': - content: - application/json: - schema: - additionalProperties: false - type: object - properties: - custom_assets: - additionalProperties: - additionalProperties: false - type: object - properties: - error: - type: string - is_deleted: - type: boolean - name: - type: string - package_name: - type: string - package_version: - type: string - sync_status: - enum: - - completed - - synchronizing - - failed - - warning - type: string - type: - type: string - required: - - type - - name - - package_name - - package_version - - sync_status - type: object - error: - type: string - integrations: - items: - additionalProperties: false - type: object - properties: - error: - type: string - id: - type: string - install_status: - additionalProperties: false - type: object - properties: - main: - type: string - remote: - type: string - required: - - main - package_name: - type: string - package_version: - type: string - sync_status: - enum: - - completed - - synchronizing - - failed - - warning - type: string - updated_at: - type: string - warning: - type: string - required: - - sync_status - - install_status - type: array - warning: - type: string - required: - - integrations - '400': - content: - application/json: - schema: - additionalProperties: false - description: Generic Error - type: object - properties: - attributes: {} - error: - type: string - errorType: - type: string - message: - type: string - statusCode: - type: number - required: - - message - - attributes - summary: Get CCR Remote synced integrations status - tags: - - CCR Remote synced integrations /api/fleet/service_tokens: post: description: '[Required authorization] Route required privileges: fleet-agents-all.' diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.test.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.test.tsx index 580eb5df52cd..77fd2e07c761 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.test.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.test.tsx @@ -12,7 +12,7 @@ import type { Output } from '../../../../types'; import { createFleetTestRendererMock } from '../../../../../../mock'; import { useFleetStatus } from '../../../../../../hooks/use_fleet_status'; import { ExperimentalFeaturesService } from '../../../../../../services'; -import { useStartServices, sendPutOutput } from '../../../../hooks'; +import { useStartServices, sendPutOutput, licenseService } from '../../../../hooks'; import { EditOutputFlyout } from '.'; @@ -99,6 +99,7 @@ describe('EditOutputFlyout', () => { beforeEach(() => { mockStartServices(false); jest.clearAllMocks(); + jest.spyOn(licenseService, 'isEnterprise').mockClear(); mockedUseFleetStatus.mockReturnValue({} as any); }); @@ -322,10 +323,11 @@ describe('EditOutputFlyout', () => { expect(utils.getByText('Additional setup required')).not.toBeNull(); }); - it('should render the flyout if the output provided is a remote ES output', async () => { + it('should render the flyout if the output provided is a remote ES output and license is at least enterprise', async () => { jest .spyOn(ExperimentalFeaturesService, 'get') .mockReturnValue({ enableSyncIntegrationsOnRemote: true } as any); + jest.spyOn(licenseService, 'isEnterprise').mockReturnValue(true); mockedUseFleetStatus.mockReturnValue({ isLoading: false, @@ -380,10 +382,48 @@ describe('EditOutputFlyout', () => { }); }); + it('should not render the flyout if the output is a remote ES output and the license is not at least enterprise', async () => { + jest + .spyOn(ExperimentalFeaturesService, 'get') + .mockReturnValue({ enableSyncIntegrationsOnRemote: true } as any); + jest.spyOn(licenseService, 'isEnterprise').mockReturnValue(false); + + mockedUseFleetStatus.mockReturnValue({ + isLoading: false, + isReady: true, + isSecretsStorageEnabled: true, + } as any); + + const { utils } = renderFlyout({ + type: 'remote_elasticsearch', + name: 'remote es output', + id: 'outputR', + is_default: false, + is_default_monitoring: false, + kibana_url: 'http://localhost', + sync_integrations: true, + }); + + remoteEsOutputLabels.forEach((label) => { + expect(utils.queryByLabelText(label)).not.toBeNull(); + }); + expect(utils.queryByTestId('serviceTokenCallout')).not.toBeNull(); + + expect(utils.queryByTestId('settingsOutputsFlyout.typeInput')?.textContent).toContain( + 'Remote Elasticsearch' + ); + + expect(utils.queryByTestId('serviceTokenSecretInput')).not.toBeNull(); + + expect(utils.queryByTestId('remoteClusterConfigurationCallout')).not.toBeInTheDocument(); + expect(utils.queryByTestId('kibanaAPIKeyCallout')).not.toBeInTheDocument(); + }); + it('should populate secret service token input with plain text value when editing remote ES output', async () => { jest .spyOn(ExperimentalFeaturesService, 'get') .mockReturnValue({ enableSyncIntegrationsOnRemote: true } as any); + jest.spyOn(licenseService, 'isEnterprise').mockReturnValue(true); mockedUseFleetStatus.mockReturnValue({ isLoading: false, diff --git a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_remote_es.tsx b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_remote_es.tsx index 1377a7d97c49..2c7c1b94b46a 100644 --- a/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_remote_es.tsx +++ b/x-pack/platform/plugins/shared/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_remote_es.tsx @@ -27,7 +27,7 @@ import { MultiRowInput } from '../multi_row_input'; import { ExperimentalFeaturesService } from '../../../../services'; -import { useStartServices } from '../../../../hooks'; +import { licenseService, useStartServices } from '../../../../hooks'; import type { OutputFormInputsType } from './use_output_form'; import { SecretFormRow } from './output_form_secret_form_row'; @@ -54,6 +54,8 @@ export const OutputFormRemoteEsSection: React.FunctionComponent = (props) sslKey: false, }); const { enableSyncIntegrationsOnRemote, enableSSLSecrets } = ExperimentalFeaturesService.get(); + const enableSyncIntegrations = enableSyncIntegrationsOnRemote && licenseService.isEnterprise(); + const [isRemoteClusterInstructionsOpen, setIsRemoteClusterInstructionsOpen] = React.useState(false); @@ -208,7 +210,7 @@ export const OutputFormRemoteEsSection: React.FunctionComponent = (props) onToggleSecretAndClearValue={onToggleSecretAndClearValue} /> - {enableSyncIntegrationsOnRemote ? ( + {enableSyncIntegrations ? ( <> = ({ const authz = useAuthz(); const { getHref } = useLink(); const { enableSyncIntegrationsOnRemote } = ExperimentalFeaturesService.get(); + const enableSyncIntegrations = enableSyncIntegrationsOnRemote && licenseService.isEnterprise(); const columns = useMemo((): Array> => { return [ @@ -121,7 +122,7 @@ export const OutputsTable: React.FunctionComponent = ({ defaultMessage: 'Status', }), }, - ...(enableSyncIntegrationsOnRemote + ...(enableSyncIntegrations ? [ { render: (output: Output) => , @@ -180,7 +181,7 @@ export const OutputsTable: React.FunctionComponent = ({ }), }, ]; - }, [deleteOutput, getHref, authz.fleet.allSettings, enableSyncIntegrationsOnRemote]); + }, [deleteOutput, getHref, authz.fleet.allSettings, enableSyncIntegrations]); return ; }; diff --git a/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/index.ts b/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/index.ts index b5b32e196ef3..1abf9c702225 100644 --- a/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/index.ts +++ b/x-pack/platform/plugins/shared/fleet/server/routes/remote_synced_integrations/index.ts @@ -15,77 +15,81 @@ import { GetRemoteSyncedIntegrationsStatusResponseSchema } from '../../types/mod import { GetRemoteSyncedIntegrationsInfoRequestSchema } from '../../types'; +import { canEnableSyncIntegrations } from '../../services/setup/fleet_synced_integrations'; + import { getRemoteSyncedIntegrationsStatusHandler, getRemoteSyncedIntegrationsInfoHandler, } from './handler'; export const registerRoutes = (router: FleetAuthzRouter) => { - router.versioned - .get({ - path: REMOTE_SYNCED_INTEGRATIONS_API_ROUTES.STATUS_PATTERN, - security: { - authz: { - requiredPrivileges: [ - FLEET_API_PRIVILEGES.SETTINGS.READ, - FLEET_API_PRIVILEGES.INTEGRATIONS.READ, - ], + if (canEnableSyncIntegrations()) { + router.versioned + .get({ + path: REMOTE_SYNCED_INTEGRATIONS_API_ROUTES.STATUS_PATTERN, + security: { + authz: { + requiredPrivileges: [ + FLEET_API_PRIVILEGES.SETTINGS.READ, + FLEET_API_PRIVILEGES.INTEGRATIONS.READ, + ], + }, }, - }, - summary: `Get CCR Remote synced integrations status`, - options: { - tags: ['oas-tag:CCR Remote synced integrations'], - }, - }) - .addVersion( - { - version: API_VERSIONS.public.v1, - validate: { - request: {}, - response: { - 200: { - body: () => GetRemoteSyncedIntegrationsStatusResponseSchema, - }, - 400: { - body: genericErrorResponse, + summary: `Get CCR Remote synced integrations status`, + options: { + tags: ['oas-tag:CCR Remote synced integrations'], + }, + }) + .addVersion( + { + version: API_VERSIONS.public.v1, + validate: { + request: {}, + response: { + 200: { + body: () => GetRemoteSyncedIntegrationsStatusResponseSchema, + }, + 400: { + body: genericErrorResponse, + }, }, }, }, - }, - getRemoteSyncedIntegrationsStatusHandler - ); + getRemoteSyncedIntegrationsStatusHandler + ); - router.versioned - .get({ - path: REMOTE_SYNCED_INTEGRATIONS_API_ROUTES.INFO_PATTERN, - security: { - authz: { - requiredPrivileges: [ - FLEET_API_PRIVILEGES.SETTINGS.READ, - FLEET_API_PRIVILEGES.INTEGRATIONS.READ, - ], + router.versioned + .get({ + path: REMOTE_SYNCED_INTEGRATIONS_API_ROUTES.INFO_PATTERN, + security: { + authz: { + requiredPrivileges: [ + FLEET_API_PRIVILEGES.SETTINGS.READ, + FLEET_API_PRIVILEGES.INTEGRATIONS.READ, + ], + }, }, - }, - summary: `Get CCR Remote synced integrations status by outputId`, - options: { - tags: ['oas-tag:CCR Remote synced integrations'], - }, - }) - .addVersion( - { - version: API_VERSIONS.public.v1, - validate: { - request: GetRemoteSyncedIntegrationsInfoRequestSchema, - response: { - 200: { - body: () => GetRemoteSyncedIntegrationsStatusResponseSchema, - }, - 400: { - body: genericErrorResponse, + summary: `Get CCR Remote synced integrations status by outputId`, + options: { + tags: ['oas-tag:CCR Remote synced integrations'], + }, + }) + .addVersion( + { + version: API_VERSIONS.public.v1, + validate: { + request: GetRemoteSyncedIntegrationsInfoRequestSchema, + response: { + 200: { + body: () => GetRemoteSyncedIntegrationsStatusResponseSchema, + }, + 400: { + body: genericErrorResponse, + }, }, }, }, - }, - getRemoteSyncedIntegrationsInfoHandler - ); + getRemoteSyncedIntegrationsInfoHandler + ); + } }; diff --git a/x-pack/platform/plugins/shared/fleet/server/services/output.ts b/x-pack/platform/plugins/shared/fleet/server/services/output.ts index b06ce8d1e414..f0cabc10ef7e 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/output.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/output.ts @@ -83,6 +83,7 @@ import { } from './secrets'; import { findAgentlessPolicies } from './outputs/helpers'; import { patchUpdateDataWithRequireEncryptedAADFields } from './outputs/so_helpers'; +import { canEnableSyncIntegrations } from './setup/fleet_synced_integrations'; type Nullable = { [P in keyof T]: T[P] | null }; @@ -367,6 +368,18 @@ async function updateAgentPoliciesDataOutputId( } } +function validateRemoteSyncIntegrationsCanBeEnabled(output: Partial) { + if ( + output.type === outputType.RemoteElasticsearch && + (output.sync_integrations === true || output.sync_uninstalled_integrations === true) && + !canEnableSyncIntegrations() + ) { + throw new OutputUnauthorizedError( + 'Remote sync integrations require at least an Enterprise license.' + ); + } +} + class OutputService { private get encryptedSoClient() { return appContextService.getInternalUserSOClient(fakeRequest); @@ -679,6 +692,8 @@ class OutputService { } } + validateRemoteSyncIntegrationsCanBeEnabled(output); + const id = options?.id ? outputIdToUuid(options.id) : SavedObjectsUtils.generateId(); // Store secret values if enabled; if not, store plain text values @@ -1103,6 +1118,7 @@ class OutputService { updateData.shipper = null; } } + validateRemoteSyncIntegrationsCanBeEnabled(data); // Store secret values if enabled; if not, store plain text values if (await isOutputSecretStorageEnabled(esClient, soClient)) { diff --git a/x-pack/platform/plugins/shared/fleet/server/services/setup/fleet_synced_integrations.test.ts b/x-pack/platform/plugins/shared/fleet/server/services/setup/fleet_synced_integrations.test.ts index 7e297c0ff864..5d58b16e16fb 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/setup/fleet_synced_integrations.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/setup/fleet_synced_integrations.test.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { licenseService } from '../license'; + import { createCCSIndexPatterns, createOrUpdateFleetSyncedIntegrationsIndex, @@ -66,72 +68,117 @@ describe('fleet_synced_integrations', () => { }; }); - it('should create index if not exists', async () => { - mockExists.mockResolvedValue(false); - - await createOrUpdateFleetSyncedIntegrationsIndex(esClientMock); - - expect(esClientMock.indices.create).toHaveBeenCalled(); - }); - - it('should update index if older version exists', async () => { - mockExists.mockResolvedValue(true); - mockGetMapping.mockResolvedValue({ - 'fleet-synced-integrations': { - mappings: { - _meta: { - version: '0.0', - }, - }, - }, + describe('with Enterprise license', () => { + beforeAll(() => { + jest.spyOn(licenseService, 'isEnterprise').mockReturnValue(true); }); - await createOrUpdateFleetSyncedIntegrationsIndex(esClientMock); + it('should create index if does not exist', async () => { + mockExists.mockResolvedValue(false); - expect(esClientMock.indices.putMapping).toHaveBeenCalled(); - }); + await createOrUpdateFleetSyncedIntegrationsIndex(esClientMock); - it('should not update index if same version exists', async () => { - mockExists.mockResolvedValue(true); - mockGetMapping.mockResolvedValue({ - 'fleet-synced-integrations': { - mappings: { - _meta: { - version: '1.0', + expect(esClientMock.indices.create).toHaveBeenCalled(); + }); + it('should update index if older version exists', async () => { + mockExists.mockResolvedValue(true); + mockGetMapping.mockResolvedValue({ + 'fleet-synced-integrations': { + mappings: { + _meta: { + version: '0.0', + }, }, }, - }, + }); + + await createOrUpdateFleetSyncedIntegrationsIndex(esClientMock); + + expect(esClientMock.indices.putMapping).toHaveBeenCalled(); }); - await createOrUpdateFleetSyncedIntegrationsIndex(esClientMock); + it('should not update index if same version exists', async () => { + mockExists.mockResolvedValue(true); + mockGetMapping.mockResolvedValue({ + 'fleet-synced-integrations': { + mappings: { + _meta: { + version: '1.0', + }, + }, + }, + }); - expect(esClientMock.indices.putMapping).not.toHaveBeenCalled(); + await createOrUpdateFleetSyncedIntegrationsIndex(esClientMock); + + expect(esClientMock.indices.putMapping).not.toHaveBeenCalled(); + }); + + it('should create index patterns for remote clusters', async () => { + await createCCSIndexPatterns(esClientMock, soClientMock, soImporterMock); + + expect(soImporterMock.import).toHaveBeenCalledWith( + expect.objectContaining({ + readStream: ['remote1:metrics-*', 'remote2:metrics-*'], + }) + ); + + expect(soClientMock.updateObjectsSpaces).toHaveBeenCalledTimes(3); + expect(soClientMock.updateObjectsSpaces).toHaveBeenCalledWith( + [{ id: 'remote1:metrics-*', type: 'index-pattern' }], + ['*'], + [] + ); + expect(soClientMock.updateObjectsSpaces).toHaveBeenCalledWith( + [{ id: 'remote2:logs-*', type: 'index-pattern' }], + ['*'], + [] + ); + expect(soClientMock.updateObjectsSpaces).toHaveBeenCalledWith( + [{ id: 'remote2:metrics-*', type: 'index-pattern' }], + ['*'], + [] + ); + }); }); - it('should create index patterns for remote clusters', async () => { - await createCCSIndexPatterns(esClientMock, soClientMock, soImporterMock); + describe('with less than Enterprise license', () => { + beforeAll(() => { + jest.spyOn(licenseService, 'isEnterprise').mockReturnValue(false); + }); - expect(soImporterMock.import).toHaveBeenCalledWith( - expect.objectContaining({ - readStream: ['remote1:metrics-*', 'remote2:metrics-*'], - }) - ); + it('should not create index', async () => { + mockExists.mockResolvedValue(false); + jest.spyOn(licenseService, 'isEnterprise').mockReturnValue(false); - expect(soClientMock.updateObjectsSpaces).toHaveBeenCalledTimes(3); - expect(soClientMock.updateObjectsSpaces).toHaveBeenCalledWith( - [{ id: 'remote1:metrics-*', type: 'index-pattern' }], - ['*'], - [] - ); - expect(soClientMock.updateObjectsSpaces).toHaveBeenCalledWith( - [{ id: 'remote2:logs-*', type: 'index-pattern' }], - ['*'], - [] - ); - expect(soClientMock.updateObjectsSpaces).toHaveBeenCalledWith( - [{ id: 'remote2:metrics-*', type: 'index-pattern' }], - ['*'], - [] - ); + await createOrUpdateFleetSyncedIntegrationsIndex(esClientMock); + + expect(esClientMock.indices.create).not.toHaveBeenCalled(); + }); + + it('should not update index', async () => { + mockExists.mockResolvedValue(true); + mockGetMapping.mockResolvedValue({ + 'fleet-synced-integrations': { + mappings: { + _meta: { + version: '0.0', + }, + }, + }, + }); + + await createOrUpdateFleetSyncedIntegrationsIndex(esClientMock); + + expect(esClientMock.indices.putMapping).not.toHaveBeenCalled(); + }); + + it('should not create index patterns for remote clusters', async () => { + await createCCSIndexPatterns(esClientMock, soClientMock, soImporterMock); + + expect(soImporterMock.import).not.toHaveBeenCalled(); + + expect(soClientMock.updateObjectsSpaces).not.toHaveBeenCalled(); + }); }); }); diff --git a/x-pack/platform/plugins/shared/fleet/server/services/setup/fleet_synced_integrations.ts b/x-pack/platform/plugins/shared/fleet/server/services/setup/fleet_synced_integrations.ts index 5de7707ef126..67b9a3347949 100644 --- a/x-pack/platform/plugins/shared/fleet/server/services/setup/fleet_synced_integrations.ts +++ b/x-pack/platform/plugins/shared/fleet/server/services/setup/fleet_synced_integrations.ts @@ -20,6 +20,7 @@ import { indexPatternTypes, } from '../epm/kibana/index_pattern/install'; import { SO_SEARCH_LIMIT } from '../../constants'; +import { licenseService } from '../license'; export const FLEET_SYNCED_INTEGRATIONS_INDEX_NAME = 'fleet-synced-integrations'; export const FLEET_SYNCED_INTEGRATIONS_CCR_INDEX_PREFIX = 'fleet-synced-integrations-ccr-*'; @@ -64,10 +65,13 @@ export const FLEET_SYNCED_INTEGRATIONS_INDEX_CONFIG = { }, }; -export async function createOrUpdateFleetSyncedIntegrationsIndex(esClient: ElasticsearchClient) { +export const canEnableSyncIntegrations = () => { const { enableSyncIntegrationsOnRemote } = appContextService.getExperimentalFeatures(); + return enableSyncIntegrationsOnRemote && licenseService.isEnterprise(); +}; - if (!enableSyncIntegrationsOnRemote) { +export async function createOrUpdateFleetSyncedIntegrationsIndex(esClient: ElasticsearchClient) { + if (!canEnableSyncIntegrations()) { return; } @@ -133,9 +137,7 @@ export async function createCCSIndexPatterns( savedObjectsClient: SavedObjectsClientContract, savedObjectsImporter: ISavedObjectsImporter ) { - const { enableSyncIntegrationsOnRemote } = appContextService.getExperimentalFeatures(); - - if (!enableSyncIntegrationsOnRemote) { + if (!canEnableSyncIntegrations()) { return; } diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.test.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.test.ts index e64bb9fd731e..439e5d4ddab4 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.test.ts @@ -14,6 +14,8 @@ import { appContextService } from '../../services/app_context'; import { getPackageSavedObjects } from '../../services/epm/packages/get'; +import { licenseService } from '../../services/license'; + import { installCustomAsset, getPipeline, getComponentTemplate } from './custom_assets'; import { getFollowerIndexInfo, @@ -1640,6 +1642,7 @@ describe('getRemoteSyncedIntegrationsStatus', () => { soClientMock = savedObjectsClientMock.create(); mockedLogger = loggerMock.create(); mockedAppContextService.getLogger.mockReturnValue(mockedLogger); + jest.spyOn(licenseService, 'isEnterprise').mockReturnValue(true); (installCustomAsset as jest.Mock).mockClear(); }); @@ -1657,6 +1660,17 @@ describe('getRemoteSyncedIntegrationsStatus', () => { }); }); + it('should return empty integrations array if license is less than Enterprise', async () => { + jest + .spyOn(mockedAppContextService, 'getExperimentalFeatures') + .mockReturnValue({ enableSyncIntegrationsOnRemote: true } as any); + jest.spyOn(licenseService, 'isEnterprise').mockReturnValue(false); + + expect(await getRemoteSyncedIntegrationsStatus(esClientMock, soClientMock)).toEqual({ + integrations: [], + }); + }); + it('should return error if there is an error in getFollowerIndexInfo', async () => { jest .spyOn(mockedAppContextService, 'getExperimentalFeatures') diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.ts index b87f3db884ce..63a8d3025d67 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/compare_synced_integrations.ts @@ -34,6 +34,8 @@ import type { } from '../../../common/types'; import { SyncStatus } from '../../../common/types'; +import { canEnableSyncIntegrations } from '../../services/setup/fleet_synced_integrations'; + import type { IntegrationsData, SyncIntegrationsData, CustomAssetsData } from './model'; import { getPipeline, getComponentTemplate, CUSTOM_ASSETS_PREFIX } from './custom_assets'; import { getFollowerIndex } from './sync_integrations_on_remote'; @@ -500,10 +502,9 @@ export const getRemoteSyncedIntegrationsStatus = async ( esClient: ElasticsearchClient, soClient: SavedObjectsClientContract ): Promise => { - const { enableSyncIntegrationsOnRemote } = appContextService.getExperimentalFeatures(); const logger = appContextService.getLogger(); - if (!enableSyncIntegrationsOnRemote) { + if (!canEnableSyncIntegrations()) { return { integrations: [] }; } diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/get_remote_status.test.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/get_remote_status.test.ts index 460a6531bde5..bb34307da358 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/get_remote_status.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/get_remote_status.test.ts @@ -18,6 +18,8 @@ import type { Output } from '../../types'; import { FleetNotFoundError } from '../../errors'; +import { licenseService } from '../../services/license'; + import { getRemoteSyncedIntegrationsInfoByOutputId } from './get_remote_status'; jest.mock('../../services/app_context'); @@ -66,6 +68,7 @@ describe('getRemoteSyncedIntegrationsInfoByOutputId', () => { soClientMock = savedObjectsClientMock.create(); mockedLogger = loggerMock.create(); mockedAppContextService.getLogger.mockReturnValue(mockedLogger); + jest.spyOn(licenseService, 'isEnterprise').mockReturnValue(true); }); afterEach(() => { @@ -82,6 +85,17 @@ describe('getRemoteSyncedIntegrationsInfoByOutputId', () => { }); }); + it('should return empty integrations array if license is not at least Enterprise', async () => { + jest + .spyOn(mockedAppContextService, 'getExperimentalFeatures') + .mockReturnValue({ enableSyncIntegrationsOnRemote: true } as any); + jest.spyOn(licenseService, 'isEnterprise').mockReturnValue(false); + + expect(await getRemoteSyncedIntegrationsInfoByOutputId(soClientMock, 'remote1')).toEqual({ + integrations: [], + }); + }); + it('should return response with error if the passed outputId is not found', async () => { jest .spyOn(mockedAppContextService, 'getExperimentalFeatures') diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/get_remote_status.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/get_remote_status.ts index 5cbb0ebd9bf4..8c710de09162 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/get_remote_status.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/get_remote_status.ts @@ -17,15 +17,15 @@ import { outputService } from '../../services'; import { FleetError, FleetNotFoundError } from '../../errors'; import type { GetRemoteSyncedIntegrationsStatusResponse } from '../../../common/types'; +import { canEnableSyncIntegrations } from '../../services/setup/fleet_synced_integrations'; export const getRemoteSyncedIntegrationsInfoByOutputId = async ( soClient: SavedObjectsClientContract, outputId: string ): Promise => { - const { enableSyncIntegrationsOnRemote } = appContextService.getExperimentalFeatures(); const logger = appContextService.getLogger(); - if (!enableSyncIntegrationsOnRemote) { + if (!canEnableSyncIntegrations()) { return { integrations: [] }; } try { diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_task.test.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_task.test.ts index 4823dd5078ba..f7cbef33576a 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_task.test.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_task.test.ts @@ -16,23 +16,33 @@ import { loggingSystemMock } from '@kbn/core/server/mocks'; import { createAppContextStartContractMock, createMockPackageService } from '../../mocks'; -import { appContextService, outputService, packagePolicyService } from '../../services'; +import { outputService } from '../../services/output'; +import { packagePolicyService } from '../../services/package_policy'; +import { appContextService } from '../../services/app_context'; + +import { licenseService } from '../../services/license'; import { SyncIntegrationsTask, TYPE, VERSION } from './sync_integrations_task'; -jest.mock('../../services', () => ({ - appContextService: { - getExperimentalFeatures: jest.fn().mockReturnValue({ enableSyncIntegrationsOnRemote: true }), - start: jest.fn(), - }, +jest.mock('../../services/output', () => ({ outputService: { list: jest.fn(), }, +})); + +jest.mock('../../services/package_policy', () => ({ packagePolicyService: { list: jest.fn(), }, })); +jest.mock('../../services/app_context', () => ({ + appContextService: { + getExperimentalFeatures: jest.fn().mockReturnValue({ enableSyncIntegrationsOnRemote: true }), + start: jest.fn(), + }, +})); + const mockOutputService = outputService as jest.Mocked; const mockPackagePolicyService = packagePolicyService as jest.Mocked; @@ -156,156 +166,212 @@ describe('SyncIntegrationsTask', () => { jest.clearAllMocks(); }); - it('Should not run if task is outdated', async () => { - const result = await runTask({ ...MOCK_TASK_INSTANCE, id: 'old-id' }); + describe('With at least Enterprise license', () => { + beforeAll(() => { + jest.spyOn(licenseService, 'isEnterprise').mockReturnValue(true); + }); + afterAll(() => { + jest.spyOn(licenseService, 'isEnterprise').mockClear(); + }); - expect(result).toEqual(getDeleteTaskRunResult()); - }); + it('Should not run if task is outdated', async () => { + const result = await runTask({ ...MOCK_TASK_INSTANCE, id: 'old-id' }); - it('Should create fleet-synced-integrations doc', async () => { - mockOutputService.list.mockResolvedValue({ - items: [ - { - type: 'remote_elasticsearch', - name: 'remote1', - hosts: ['https://remote1:9200'], - sync_integrations: true, - }, - { - type: 'remote_elasticsearch', - name: 'remote2', - hosts: ['https://remote2:9200'], - sync_integrations: false, - }, - ], - } as any); - mockPackagePolicyService.list.mockResolvedValue({ - items: [ - { - package: { - name: 'filestream', - version: '1.1.0', + expect(result).toEqual(getDeleteTaskRunResult()); + }); + + it('Should create fleet-synced-integrations doc', async () => { + mockOutputService.list.mockResolvedValue({ + items: [ + { + type: 'remote_elasticsearch', + name: 'remote1', + hosts: ['https://remote1:9200'], + sync_integrations: true, }, - inputs: [ - { - streams: [ - { - vars: { - pipeline: { - value: 'filestream-pipeline1', + { + type: 'remote_elasticsearch', + name: 'remote2', + hosts: ['https://remote2:9200'], + sync_integrations: false, + }, + ], + } as any); + mockPackagePolicyService.list.mockResolvedValue({ + items: [ + { + package: { + name: 'filestream', + version: '1.1.0', + }, + inputs: [ + { + streams: [ + { + vars: { + pipeline: { + value: 'filestream-pipeline1', + }, }, }, + ], + }, + ], + }, + ], + } as any); + await runTask(); + + expect(esClient.index).toHaveBeenCalledWith( + { + id: 'fleet-synced-integrations', + index: 'fleet-synced-integrations', + body: { + integrations: [ + { + package_name: 'system', + package_version: '0.1.0', + updated_at: expect.any(String), + install_status: 'installed', + }, + { + package_name: 'package-2', + package_version: '0.2.0', + updated_at: expect.any(String), + install_status: 'installed', + }, + ], + remote_es_hosts: [ + { + hosts: ['https://remote1:9200'], + name: 'remote1', + sync_integrations: true, + sync_uninstalled_integrations: false, + }, + { + hosts: ['https://remote2:9200'], + name: 'remote2', + sync_integrations: false, + sync_uninstalled_integrations: false, + }, + ], + custom_assets: { + 'component_template:logs-system.auth@custom': { + is_deleted: false, + name: 'logs-system.auth@custom', + package_name: 'system', + package_version: '0.1.0', + template: {}, + type: 'component_template', + }, + 'ingest_pipeline:logs-system.auth@custom': { + is_deleted: false, + name: 'logs-system.auth@custom', + package_name: 'system', + package_version: '0.1.0', + pipeline: { + processors: [], }, - ], - }, - ], - }, - ], - } as any); - await runTask(); - - expect(esClient.index).toHaveBeenCalledWith( - { - id: 'fleet-synced-integrations', - index: 'fleet-synced-integrations', - body: { - integrations: [ - { - package_name: 'system', - package_version: '0.1.0', - updated_at: expect.any(String), - install_status: 'installed', - }, - { - package_name: 'package-2', - package_version: '0.2.0', - updated_at: expect.any(String), - install_status: 'installed', - }, - ], - remote_es_hosts: [ - { - hosts: ['https://remote1:9200'], - name: 'remote1', - sync_integrations: true, - sync_uninstalled_integrations: false, - }, - { - hosts: ['https://remote2:9200'], - name: 'remote2', - sync_integrations: false, - sync_uninstalled_integrations: false, - }, - ], - custom_assets: { - 'component_template:logs-system.auth@custom': { - is_deleted: false, - name: 'logs-system.auth@custom', - package_name: 'system', - package_version: '0.1.0', - template: {}, - type: 'component_template', - }, - 'ingest_pipeline:logs-system.auth@custom': { - is_deleted: false, - name: 'logs-system.auth@custom', - package_name: 'system', - package_version: '0.1.0', - pipeline: { - processors: [], + type: 'ingest_pipeline', }, - type: 'ingest_pipeline', - }, - 'ingest_pipeline:filestream-pipeline1': { - is_deleted: false, - name: 'filestream-pipeline1', - package_name: 'filestream', - package_version: '1.1.0', - pipeline: { - processors: [], + 'ingest_pipeline:filestream-pipeline1': { + is_deleted: false, + name: 'filestream-pipeline1', + package_name: 'filestream', + package_version: '1.1.0', + pipeline: { + processors: [], + }, + type: 'ingest_pipeline', }, - type: 'ingest_pipeline', }, }, }, - }, - expect.anything() - ); - }); + expect.anything() + ); + }); - it('Should save custom assets error', async () => { - mockOutputService.list.mockResolvedValue({ - items: [ + it('Should save custom assets error', async () => { + mockOutputService.list.mockResolvedValue({ + items: [ + { + type: 'remote_elasticsearch', + name: 'remote1', + hosts: ['https://remote1:9200'], + sync_integrations: true, + }, + ], + } as any); + esClient.ingest.getPipeline.mockRejectedValue(new Error('es error')); + await runTask(); + + expect(esClient.index).toHaveBeenCalledWith( { - type: 'remote_elasticsearch', - name: 'remote1', - hosts: ['https://remote1:9200'], - sync_integrations: true, + id: 'fleet-synced-integrations', + index: 'fleet-synced-integrations', + body: { + integrations: [ + { + package_name: 'system', + package_version: '0.1.0', + updated_at: expect.any(String), + install_status: 'installed', + }, + { + package_name: 'package-2', + package_version: '0.2.0', + updated_at: expect.any(String), + install_status: 'installed', + }, + ], + remote_es_hosts: [ + { + hosts: ['https://remote1:9200'], + name: 'remote1', + sync_integrations: true, + sync_uninstalled_integrations: false, + }, + ], + custom_assets: {}, + custom_assets_error: { + timestamp: expect.any(String), + error: 'es error', + }, + }, }, - ], - } as any); - esClient.ingest.getPipeline.mockRejectedValue(new Error('es error')); - await runTask(); + expect.anything() + ); + }); - expect(esClient.index).toHaveBeenCalledWith( - { - id: 'fleet-synced-integrations', - index: 'fleet-synced-integrations', - body: { - integrations: [ - { - package_name: 'system', - package_version: '0.1.0', - updated_at: expect.any(String), - install_status: 'installed', - }, - { - package_name: 'package-2', - package_version: '0.2.0', - updated_at: expect.any(String), - install_status: 'installed', - }, - ], + it('Should not index fleet-synced-integrations doc if no outputs with sync enabled', async () => { + mockOutputService.list.mockResolvedValue({ + items: [ + { + type: 'remote_elasticsearch', + name: 'remote2', + hosts: ['https://remote2:9200'], + sync_integrations: false, + }, + ], + } as any); + await runTask(); + + expect(esClient.index).not.toHaveBeenCalled(); + }); + + it('Should index fleet-synced-integrations doc if sync flag changed from true to false', async () => { + mockOutputService.list.mockResolvedValue({ + items: [ + { + type: 'remote_elasticsearch', + name: 'remote2', + hosts: ['https://remote2:9200'], + sync_integrations: false, + }, + ], + } as any); + esClient.get.mockResolvedValue({ + _source: { remote_es_hosts: [ { hosts: ['https://remote1:9200'], @@ -314,209 +380,307 @@ describe('SyncIntegrationsTask', () => { sync_uninstalled_integrations: false, }, ], + }, + } as any); + await runTask(); + + expect(esClient.index).toHaveBeenCalled(); + }); + + it('Should not index fleet-synced-integrations doc if sync flag already false', async () => { + mockOutputService.list.mockResolvedValue({ + items: [ + { + type: 'remote_elasticsearch', + name: 'remote2', + hosts: ['https://remote2:9200'], + sync_integrations: false, + }, + ], + } as any); + esClient.get.mockResolvedValue({ + _source: { + remote_es_hosts: [ + { hosts: ['https://remote1:9200'], name: 'remote1', sync_integrations: false }, + ], + integrations: [], custom_assets: {}, - custom_assets_error: { - timestamp: expect.any(String), - error: 'es error', - }, + custom_assets_error: {}, }, - }, - expect.anything() - ); - }); + } as any); + await runTask(); - it('Should not index fleet-synced-integrations doc if no outputs with sync enabled', async () => { - mockOutputService.list.mockResolvedValue({ - items: [ - { - type: 'remote_elasticsearch', - name: 'remote2', - hosts: ['https://remote2:9200'], - sync_integrations: false, - }, - ], - } as any); - await runTask(); + expect(esClient.index).not.toHaveBeenCalled(); + }); - expect(esClient.index).not.toHaveBeenCalled(); - }); - - it('Should index fleet-synced-integrations doc if sync flag changed from true to false', async () => { - mockOutputService.list.mockResolvedValue({ - items: [ - { - type: 'remote_elasticsearch', - name: 'remote2', - hosts: ['https://remote2:9200'], - sync_integrations: false, - }, - ], - } as any); - esClient.get.mockResolvedValue({ - _source: { - remote_es_hosts: [ - { hosts: ['https://remote1:9200'], name: 'remote1', sync_integrations: true }, - ], - }, - } as any); - await runTask(); - - expect(esClient.index).toHaveBeenCalled(); - }); - - it('Should not index fleet-synced-integrations doc if sync flag already false', async () => { - mockOutputService.list.mockResolvedValue({ - items: [ - { - type: 'remote_elasticsearch', - name: 'remote2', - hosts: ['https://remote2:9200'], - sync_integrations: false, - }, - ], - } as any); - esClient.get.mockResolvedValue({ - _source: { - remote_es_hosts: [ - { hosts: ['https://remote1:9200'], name: 'remote1', sync_integrations: false }, - ], - integrations: [], - custom_assets: {}, - custom_assets_error: {}, - }, - } as any); - await runTask(); - - expect(esClient.index).not.toHaveBeenCalled(); - }); - - it('Should not index fleet-synced-integrations doc if sync doc does not exist', async () => { - mockOutputService.list.mockResolvedValue({ - items: [ - { - type: 'remote_elasticsearch', - name: 'remote2', - hosts: ['https://remote2:9200'], - sync_integrations: false, - }, - ], - } as any); - esClient.get.mockRejectedValue({ statusCode: 404 }); - await runTask(); - - expect(esClient.index).not.toHaveBeenCalled(); - }); - - it('Should mark removed integrations as uninstalled if uninstall syncing is enabled', async () => { - mockOutputService.list.mockResolvedValue({ - items: [ - { - type: 'remote_elasticsearch', - name: 'remote1', - hosts: ['https://remote1:9200'], - sync_integrations: true, - sync_uninstalled_integrations: true, - }, - ], - } as any); - esClient.get.mockResolvedValue({ - _source: { - remote_es_hosts: [ - { hosts: ['https://remote1:9200'], name: 'remote1', sync_integrations: false }, - ], - integrations: [ + it('Should not index fleet-synced-integrations doc if sync doc does not exist', async () => { + mockOutputService.list.mockResolvedValue({ + items: [ { - package_name: 'system', - package_version: '0.1.0', - updated_at: new Date().toISOString(), - install_status: 'installed', - }, - { - package_name: 'package-2', - package_version: '0.2.0', - updated_at: new Date().toISOString(), - install_status: 'installed', - }, - { - package_name: 'package-3', - package_version: '0.3.0', - updated_at: new Date().toISOString(), - install_status: 'installed', + type: 'remote_elasticsearch', + name: 'remote2', + hosts: ['https://remote2:9200'], + sync_integrations: false, }, ], - custom_assets: {}, - custom_assets_error: {}, - }, - } as any); - await runTask(); + } as any); + esClient.get.mockRejectedValue({ statusCode: 404 }); + await runTask(); - expect(esClient.index).toHaveBeenCalledWith( - { - id: 'fleet-synced-integrations', - index: 'fleet-synced-integrations', - body: { + expect(esClient.index).not.toHaveBeenCalled(); + }); + + it('Should mark removed integrations as uninstalled if uninstall syncing is enabled', async () => { + mockOutputService.list.mockResolvedValue({ + items: [ + { + type: 'remote_elasticsearch', + name: 'remote1', + hosts: ['https://remote1:9200'], + sync_integrations: true, + sync_uninstalled_integrations: true, + }, + ], + } as any); + esClient.get.mockResolvedValue({ + _source: { + remote_es_hosts: [ + { hosts: ['https://remote1:9200'], name: 'remote1', sync_integrations: false }, + ], integrations: [ { package_name: 'system', package_version: '0.1.0', - updated_at: expect.any(String), + updated_at: new Date().toISOString(), install_status: 'installed', }, { package_name: 'package-2', package_version: '0.2.0', - updated_at: expect.any(String), + updated_at: new Date().toISOString(), install_status: 'installed', }, { package_name: 'package-3', package_version: '0.3.0', - updated_at: expect.any(String), - install_status: 'not_installed', + updated_at: new Date().toISOString(), + install_status: 'installed', }, ], - remote_es_hosts: [ - { - hosts: ['https://remote1:9200'], - name: 'remote1', - sync_integrations: true, - sync_uninstalled_integrations: true, - }, - ], - custom_assets: { - 'component_template:logs-system.auth@custom': { - is_deleted: false, - name: 'logs-system.auth@custom', - package_name: 'system', - package_version: '0.1.0', - template: {}, - type: 'component_template', - }, - 'ingest_pipeline:filestream-pipeline1': { - is_deleted: false, - name: 'filestream-pipeline1', - package_name: 'filestream', - package_version: '1.1.0', - pipeline: { - processors: [], + custom_assets: {}, + custom_assets_error: {}, + }, + } as any); + await runTask(); + + expect(esClient.index).toHaveBeenCalledWith( + { + id: 'fleet-synced-integrations', + index: 'fleet-synced-integrations', + body: { + integrations: [ + { + package_name: 'system', + package_version: '0.1.0', + updated_at: expect.any(String), + install_status: 'installed', }, - type: 'ingest_pipeline', - }, - 'ingest_pipeline:logs-system.auth@custom': { - is_deleted: false, - name: 'logs-system.auth@custom', - package_name: 'system', - package_version: '0.1.0', - pipeline: { - processors: [], + { + package_name: 'package-2', + package_version: '0.2.0', + updated_at: expect.any(String), + install_status: 'installed', + }, + { + package_name: 'package-3', + package_version: '0.3.0', + updated_at: expect.any(String), + install_status: 'not_installed', + }, + ], + remote_es_hosts: [ + { + hosts: ['https://remote1:9200'], + name: 'remote1', + sync_integrations: true, + sync_uninstalled_integrations: true, + }, + ], + custom_assets: { + 'component_template:logs-system.auth@custom': { + is_deleted: false, + name: 'logs-system.auth@custom', + package_name: 'system', + package_version: '0.1.0', + template: {}, + type: 'component_template', + }, + 'ingest_pipeline:filestream-pipeline1': { + is_deleted: false, + name: 'filestream-pipeline1', + package_name: 'filestream', + package_version: '1.1.0', + pipeline: { + processors: [], + }, + type: 'ingest_pipeline', + }, + 'ingest_pipeline:logs-system.auth@custom': { + is_deleted: false, + name: 'logs-system.auth@custom', + package_name: 'system', + package_version: '0.1.0', + pipeline: { + processors: [], + }, + type: 'ingest_pipeline', }, - type: 'ingest_pipeline', }, }, }, - }, - expect.anything() - ); + expect.anything() + ); + }); + }); + + describe('With less than Enterprise license', () => { + beforeAll(() => { + jest.spyOn(licenseService, 'isEnterprise').mockReturnValue(false); + }); + afterAll(() => { + jest.spyOn(licenseService, 'isEnterprise').mockClear(); + }); + + it('Should not create fleet-synced-integrations doc', async () => { + mockOutputService.list.mockResolvedValue({ + items: [ + { + type: 'remote_elasticsearch', + name: 'remote1', + hosts: ['https://remote1:9200'], + sync_integrations: true, + }, + { + type: 'remote_elasticsearch', + name: 'remote2', + hosts: ['https://remote2:9200'], + sync_integrations: false, + }, + ], + } as any); + mockPackagePolicyService.list.mockResolvedValue({ + items: [ + { + package: { + name: 'filestream', + version: '1.1.0', + }, + inputs: [ + { + streams: [ + { + vars: { + pipeline: { + value: 'filestream-pipeline1', + }, + }, + }, + ], + }, + ], + }, + ], + } as any); + await runTask(); + + expect(esClient.index).not.toHaveBeenCalled(); + }); + + it('Should not save custom assets error', async () => { + mockOutputService.list.mockResolvedValue({ + items: [ + { + type: 'remote_elasticsearch', + name: 'remote1', + hosts: ['https://remote1:9200'], + sync_integrations: true, + }, + ], + } as any); + esClient.ingest.getPipeline.mockRejectedValue(new Error('es error')); + await runTask(); + + expect(esClient.index).not.toHaveBeenCalled(); + }); + + it('Should not index fleet-synced-integrations doc even if sync flag changed', async () => { + mockOutputService.list.mockResolvedValue({ + items: [ + { + type: 'remote_elasticsearch', + name: 'remote2', + hosts: ['https://remote2:9200'], + sync_integrations: false, + }, + ], + } as any); + esClient.get.mockResolvedValue({ + _source: { + remote_es_hosts: [ + { hosts: ['https://remote1:9200'], name: 'remote1', sync_integrations: true }, + ], + }, + } as any); + await runTask(); + + expect(esClient.index).not.toHaveBeenCalled(); + }); + + it('Should mark removed integrations as uninstalled if uninstall syncing is enabled', async () => { + mockOutputService.list.mockResolvedValue({ + items: [ + { + type: 'remote_elasticsearch', + name: 'remote1', + hosts: ['https://remote1:9200'], + sync_integrations: true, + sync_uninstalled_integrations: true, + }, + ], + } as any); + esClient.get.mockResolvedValue({ + _source: { + remote_es_hosts: [ + { hosts: ['https://remote1:9200'], name: 'remote1', sync_integrations: false }, + ], + integrations: [ + { + package_name: 'system', + package_version: '0.1.0', + updated_at: new Date().toISOString(), + install_status: 'installed', + }, + { + package_name: 'package-2', + package_version: '0.2.0', + updated_at: new Date().toISOString(), + install_status: 'installed', + }, + { + package_name: 'package-3', + package_version: '0.3.0', + updated_at: new Date().toISOString(), + install_status: 'installed', + }, + ], + custom_assets: {}, + custom_assets_error: {}, + }, + } as any); + await runTask(); + + expect(esClient.index).not.toHaveBeenCalled(); + }); }); }); }); diff --git a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_task.ts b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_task.ts index 18691ab4bb24..bdc1da8d151a 100644 --- a/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_task.ts +++ b/x-pack/platform/plugins/shared/fleet/server/tasks/sync_integrations/sync_integrations_task.ts @@ -19,9 +19,12 @@ import { errors } from '@elastic/elasticsearch'; import { SO_SEARCH_LIMIT, outputType } from '../../../common/constants'; import type { NewRemoteElasticsearchOutput } from '../../../common/types'; -import { appContextService, outputService } from '../../services'; +import { outputService } from '../../services'; import { getInstalledPackageSavedObjects } from '../../services/epm/packages/get'; -import { FLEET_SYNCED_INTEGRATIONS_INDEX_NAME } from '../../services/setup/fleet_synced_integrations'; +import { + FLEET_SYNCED_INTEGRATIONS_INDEX_NAME, + canEnableSyncIntegrations, +} from '../../services/setup/fleet_synced_integrations'; import { syncIntegrationsOnRemote } from './sync_integrations_on_remote'; import { getCustomAssets } from './custom_assets'; @@ -123,9 +126,7 @@ export class SyncIntegrationsTask { const esClient = coreStart.elasticsearch.client.asInternalUser; const soClient = new SavedObjectsClient(coreStart.savedObjects.createInternalRepository()); - const { enableSyncIntegrationsOnRemote } = appContextService.getExperimentalFeatures(); - - if (!enableSyncIntegrationsOnRemote) { + if (!canEnableSyncIntegrations()) { return; } diff --git a/x-pack/test/fleet_api_integration/apis/outputs/crud.ts b/x-pack/test/fleet_api_integration/apis/outputs/crud.ts index bde6984eb1bf..72e4f1eeedfb 100644 --- a/x-pack/test/fleet_api_integration/apis/outputs/crud.ts +++ b/x-pack/test/fleet_api_integration/apis/outputs/crud.ts @@ -792,7 +792,7 @@ export default function (providerContext: FtrProviderContext) { } }); - it('should allow to update kibana_api_key on an existing remote_elasticsearch output', async function () { + it('should not allow to update kibana_api_key on an existing remote_elasticsearch output if the license is not at least enterprise', async function () { const res = await supertest .post(`/api/fleet/outputs`) .set('kbn-xsrf', 'xxxx') @@ -800,13 +800,11 @@ export default function (providerContext: FtrProviderContext) { name: 'Remote Output With kibana_api_key', type: 'remote_elasticsearch', hosts: ['https://test.fr:443'], - sync_integrations: true, kibana_url: 'https://testhost', - kibana_api_key: 'aaaa', }) .expect(200); const outputId = res.body.item.id; - const updatedRes = await supertest + await supertest .put(`/api/fleet/outputs/${outputId}`) .set('kbn-xsrf', 'xxxx') .send({ @@ -817,8 +815,7 @@ export default function (providerContext: FtrProviderContext) { kibana_url: 'https://testhost', kibana_api_key: 'bbbb', }) - .expect(200); - expect(updatedRes.body.item.kibana_api_key).to.equal('bbbb'); + .expect(400); }); it('should bump all policies in all spaces if updating the default output', async () => { @@ -1824,7 +1821,7 @@ export default function (providerContext: FtrProviderContext) { .expect(200); }); - it('should allow to create a new remote_elasticsearch output with kibana_api_key field', async function () { + it('should not allow to create a new remote_elasticsearch output with kibana_api_key if the license is not at least enterprise', async function () { await supertest .post(`/api/fleet/outputs`) .set('kbn-xsrf', 'xxxx') @@ -1836,7 +1833,7 @@ export default function (providerContext: FtrProviderContext) { kibana_url: 'https://testhost', kibana_api_key: 'aaaa', }) - .expect(200); + .expect(400); }); });