diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml index 866386232dc7df..94753297ce85f0 100644 --- a/.buildkite/ftr_configs.yml +++ b/.buildkite/ftr_configs.yml @@ -410,10 +410,12 @@ enabled: - x-pack/test_serverless/api_integration/test_suites/observability/config.ts - x-pack/test_serverless/api_integration/test_suites/observability/config.feature_flags.ts - x-pack/test_serverless/api_integration/test_suites/observability/common_configs/config.group1.ts + - x-pack/test_serverless/api_integration/test_suites/observability/fleet/config.ts - x-pack/test_serverless/api_integration/test_suites/search/config.ts - x-pack/test_serverless/api_integration/test_suites/search/common_configs/config.group1.ts - x-pack/test_serverless/api_integration/test_suites/security/config.ts - x-pack/test_serverless/api_integration/test_suites/security/common_configs/config.group1.ts + - x-pack/test_serverless/api_integration/test_suites/security/fleet/config.ts - x-pack/test_serverless/functional/test_suites/observability/config.ts - x-pack/test_serverless/functional/test_suites/observability/config.examples.ts - x-pack/test_serverless/functional/test_suites/observability/config.saved_objects_management.ts diff --git a/config/serverless.yml b/config/serverless.yml index 0a1dd4aff8ca93..1d98bcca216e74 100644 --- a/config/serverless.yml +++ b/config/serverless.yml @@ -4,7 +4,6 @@ xpack.serverless.plugin.enabled: true # Fleet settings xpack.fleet.internal.fleetServerStandalone: true xpack.fleet.internal.disableILMPolicies: true -xpack.fleet.internal.disableProxies: true xpack.fleet.internal.activeAgentsSoftLimit: 25000 xpack.fleet.internal.onlyAllowAgentUpgradeToKnownVersions: true xpack.fleet.internal.retrySetupOnBoot: true diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts index bc13a0c5a8e2a1..e6434471a08cfc 100644 --- a/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -257,7 +257,6 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'xpack.fleet.agents.enabled (boolean)', 'xpack.fleet.enableExperimental (array)', 'xpack.fleet.internal.activeAgentsSoftLimit (number)', - 'xpack.fleet.internal.disableProxies (boolean)', 'xpack.fleet.internal.fleetServerStandalone (boolean)', 'xpack.fleet.internal.onlyAllowAgentUpgradeToKnownVersions (boolean)', 'xpack.fleet.developer.maxAgentPoliciesWithInactivityTimeout (number)', diff --git a/x-pack/plugins/fleet/README.md b/x-pack/plugins/fleet/README.md index ebe65d77e3bbf6..07dd4bfd67c005 100644 --- a/x-pack/plugins/fleet/README.md +++ b/x-pack/plugins/fleet/README.md @@ -170,9 +170,11 @@ You can also run a specific test by passing the filepath as an argument, e.g.: yarn jest --config x-pack/plugins/fleet/jest.config.js x-pack/plugins/fleet/common/services/validate_package_policy.test.ts ``` -#### API integration tests +#### API integration tests (stateful) -You need to have `docker` to run ingest manager api integration tests. +API integration tests are run using the functional test runner (FTR). When developing or troubleshooting tests, it is convenient to run the server and tests separately as detailed below. + +Note: Docker needs to be running to run these tests. 1. In one terminal, run the server from the Kibana root directory with @@ -188,22 +190,38 @@ You need to have `docker` to run ingest manager api integration tests. 1. In a second terminal, run the tests from the Kibana root directory with - ``` + ```bash FLEET_PACKAGE_REGISTRY_PORT=12345 yarn test:ftr:runner --config x-pack/test/fleet_api_integration/ ``` Optionally, you can filter which tests you want to run using `--grep` - ``` + ```bash FLEET_PACKAGE_REGISTRY_PORT=12345 yarn test:ftr:runner --config x-pack/test/fleet_api_integration/ --grep='fleet' ``` -**Note** you can also supply which docker image to use for the package registry via the `FLEET_PACKAGE_REGISTRY_DOCKER_IMAGE` env variable. For example, +Note: you can also supply which Docker image to use for the Package Registry via the `FLEET_PACKAGE_REGISTRY_DOCKER_IMAGE` env variable. For example, -``` +```bash FLEET_PACKAGE_REGISTRY_DOCKER_IMAGE='docker.elastic.co/package-registry/distribution:production' FLEET_PACKAGE_REGISTRY_PORT=12345 yarn test:ftr:runner ``` +#### API integration tests (serverless) + +The process for running serverless API integration tests is similar as above. Security and observability project types have Fleet enabled. At the time of writing, the same tests exist for Fleet under these two project types. + +Security: +```bash +FLEET_PACKAGE_REGISTRY_PORT=12345 yarn test:ftr:server --config x-pack/test_serverless/api_integration/test_suites/security/fleet/config.ts +FLEET_PACKAGE_REGISTRY_PORT=12345 yarn test:ftr:runner --config x-pack/test_serverless/api_integration/test_suites/security/fleet/config.ts +``` + +Observability: +```bash +FLEET_PACKAGE_REGISTRY_PORT=12345 yarn test:ftr:server --config x-pack/test_serverless/api_integration/test_suites/observability/fleet/config.ts +FLEET_PACKAGE_REGISTRY_PORT=12345 yarn test:ftr:runner --config x-pack/test_serverless/api_integration/test_suites/observability/fleet/config.ts +``` + #### Cypress tests We support UI end-to-end testing with Cypress. Refer to [cypress/README.md](./cypress/README.md) for how to run these tests. diff --git a/x-pack/plugins/fleet/common/constants/fleet_server_policy_config.ts b/x-pack/plugins/fleet/common/constants/fleet_server_policy_config.ts index db8cacb91d6d7d..11582a2631dfae 100644 --- a/x-pack/plugins/fleet/common/constants/fleet_server_policy_config.ts +++ b/x-pack/plugins/fleet/common/constants/fleet_server_policy_config.ts @@ -12,3 +12,5 @@ export const DEFAULT_FLEET_SERVER_HOST_ID = 'fleet-default-fleet-server-host'; export const FLEET_PROXY_SAVED_OBJECT_TYPE = 'fleet-proxy'; export const PROXY_URL_REGEX = /^(http[s]?|socks5):\/\/[^\s$.?#].[^\s]*$/gm; + +export const SERVERLESS_DEFAULT_FLEET_SERVER_HOST_ID = 'default-fleet-server'; diff --git a/x-pack/plugins/fleet/common/constants/output.ts b/x-pack/plugins/fleet/common/constants/output.ts index cdd52a42f3e6c4..aa54126a6060ba 100644 --- a/x-pack/plugins/fleet/common/constants/output.ts +++ b/x-pack/plugins/fleet/common/constants/output.ts @@ -26,6 +26,8 @@ export const DEFAULT_OUTPUT: NewOutput = { hosts: [''], }; +export const SERVERLESS_DEFAULT_OUTPUT_ID = 'es-default-output'; + export const LICENCE_FOR_PER_POLICY_OUTPUT = 'platinum'; /** diff --git a/x-pack/plugins/fleet/common/types/index.ts b/x-pack/plugins/fleet/common/types/index.ts index b2c66a01dfcaab..61e28828492362 100644 --- a/x-pack/plugins/fleet/common/types/index.ts +++ b/x-pack/plugins/fleet/common/types/index.ts @@ -51,7 +51,6 @@ export interface FleetConfigType { }; internal?: { disableILMPolicies: boolean; - disableProxies: boolean; fleetServerStandalone: boolean; onlyAllowAgentUpgradeToKnownVersions: boolean; activeAgentsSoftLimit?: number; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.tsx index 8013a490103afd..76fd2911551390 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/search_and_filter_bar.tsx @@ -21,7 +21,6 @@ import { useIsFirstTimeAgentUserQuery } from '../../../../../integrations/sectio import type { Agent, AgentPolicy } from '../../../../types'; import { SearchBar } from '../../../../components'; import { AGENTS_INDEX, AGENTS_PREFIX } from '../../../../constants'; -import { useFleetServerStandalone } from '../../../../hooks'; import { AgentBulkActions } from './bulk_actions'; import type { SelectionMode } from './types'; @@ -90,10 +89,8 @@ export const SearchAndFilterBar: React.FunctionComponent { - const { isFleetServerStandalone } = useFleetServerStandalone(); const { isFirstTimeAgentUser, isLoading: isFirstTimeAgentUserLoading } = useIsFirstTimeAgentUserQuery(); - const showAddFleetServerBtn = !isFleetServerStandalone; return ( <> @@ -110,25 +107,23 @@ export const SearchAndFilterBar: React.FunctionComponent - {showAddFleetServerBtn && ( - - - } - > - - - - - - )} + + + } + > + + + + + void; proxies: FleetProxy[]; } export const EditOutputFlyout: React.FunctionComponent = ({ + defaultOuput, onClose, output, proxies, }) => { useBreadcrumbs('settings'); - const form = useOutputForm(onClose, output); + const form = useOutputForm(onClose, output, defaultOuput); const inputs = form.inputs; const { docLinks, cloud } = useStartServices(); const { euiTheme } = useEuiTheme(); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_elasticsearch.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_elasticsearch.tsx index 877c285e391c6c..21e43fcba001ae 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_elasticsearch.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_elasticsearch.tsx @@ -36,6 +36,14 @@ export const OutputFormElasticsearchSection: React.FunctionComponent = (p )} {...inputs.elasticsearchUrlInput.props} isUrl + helpText={ + inputs.elasticsearchUrlInput.props.disabled && ( + + ) + } /> void, output?: Output) { +export function useOutputForm(onSucess: () => void, output?: Output, defaultOuput?: Output) { const fleetStatus = useFleetStatus(); const { showExperimentalShipperOptions } = ExperimentalFeaturesService.get(); @@ -166,7 +166,7 @@ export function useOutputForm(onSucess: () => void, output?: Output) { ); const [isLoading, setIsloading] = useState(false); - const { notifications } = useStartServices(); + const { notifications, cloud } = useStartServices(); const { confirm } = useConfirmModal(); // preconfigured output do not allow edition @@ -208,11 +208,17 @@ export function useOutputForm(onSucess: () => void, output?: Output) { validateCATrustedFingerPrint, isDisabled('ca_trusted_fingerprint') ); + // ES output's host URL is restricted to default in serverless + const isServerless = cloud?.isServerlessEnabled; + // Set the hosts to default for new ES output in serverless. + const elasticsearchUrlDefaultValue = + isServerless && !output?.hosts ? defaultOuput?.hosts || [] : output?.hosts || []; + const elasticsearchUrlDisabled = isServerless || isDisabled('hosts'); const elasticsearchUrlInput = useComboInput( 'esHostsComboxBox', - output?.hosts ?? [], + elasticsearchUrlDefaultValue, validateESHosts, - isDisabled('hosts') + elasticsearchUrlDisabled ); const presetInput = useInput( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/index.tsx index f8dfc630f5a513..f564bdd8cbd748 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/index.tsx @@ -40,17 +40,19 @@ import { useFleetServerHostsForm } from './use_fleet_server_host_form'; export interface FleetServerHostsFlyoutProps { onClose: () => void; fleetServerHost?: FleetServerHost; + defaultFleetServerHost?: FleetServerHost; proxies: FleetProxy[]; } export const FleetServerHostsFlyout: React.FunctionComponent = ({ onClose, fleetServerHost, + defaultFleetServerHost, proxies, }) => { - const { docLinks } = useStartServices(); + const { docLinks, cloud } = useStartServices(); - const form = useFleetServerHostsForm(fleetServerHost, onClose); + const form = useFleetServerHostsForm(fleetServerHost, onClose, defaultFleetServerHost); const { inputs } = form; const proxiesOptions = useMemo( @@ -61,39 +63,54 @@ export const FleetServerHostsFlyout: React.FunctionComponent - -

- {fleetServerHost ? ( - - ) : ( - - )} -

-
+ <> + +

+ {fleetServerHost ? ( + + ) : ( + + )} +

+
+ {!fleetServerHost && ( + <> + + + + + + )} +
- + } + > - } - > - - + + )} <> - - - - - ), - }} - /> - - + {!cloud?.isServerlessEnabled && ( + <> + + + + + ), + }} + /> + + + + )} + ) + } />
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/use_fleet_server_host_form.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/use_fleet_server_host_form.tsx index 5a85fa3b9e111b..781d6d6141ffe8 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/use_fleet_server_host_form.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/use_fleet_server_host_form.tsx @@ -115,10 +115,11 @@ export function validateName(value: string) { export function useFleetServerHostsForm( fleetServerHost: FleetServerHost | undefined, - onSuccess: () => void + onSuccess: () => void, + defaultFleetServerHost?: FleetServerHost ) { const [isLoading, setIsLoading] = useState(false); - const { notifications } = useStartServices(); + const { notifications, cloud } = useStartServices(); const { confirm } = useConfirmModal(); const isPreconfigured = fleetServerHost?.is_preconfigured ?? false; @@ -128,11 +129,18 @@ export function useFleetServerHostsForm( isPreconfigured || fleetServerHost?.is_default ); + const isServerless = cloud?.isServerlessEnabled; + // Set the host URLs to default for new Fleet server host in serverless. + const hostUrlsDefaultValue = + isServerless && !fleetServerHost?.host_urls + ? defaultFleetServerHost?.host_urls || [] + : fleetServerHost?.host_urls || []; + const hostUrlsDisabled = isPreconfigured || isServerless; const hostUrlsInput = useComboInput( 'hostUrls', - fleetServerHost?.host_urls || [], + hostUrlsDefaultValue, validateFleetServerHosts, - isPreconfigured + hostUrlsDisabled ); const proxyIdInput = useInput(fleetServerHost?.proxy_id ?? '', () => undefined, isPreconfigured); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/fleet_server_hosts_section.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/fleet_server_hosts_section.tsx index e748c379215026..1114efb814a217 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/fleet_server_hosts_section.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/fleet_server_hosts_section.tsx @@ -11,7 +11,7 @@ import { EuiTitle, EuiLink, EuiText, EuiSpacer, EuiButtonEmpty } from '@elastic/ import { FormattedMessage } from '@kbn/i18n-react'; import type { FleetServerHost } from '../../../../types'; -import { useLink, useStartServices, useConfig } from '../../../../hooks'; +import { useLink, useStartServices } from '../../../../hooks'; import { FleetServerHostsTable } from '../fleet_server_hosts_table'; export interface FleetServerHostsSectionProps { @@ -25,7 +25,6 @@ export const FleetServerHostsSection: React.FunctionComponent { const { docLinks } = useStartServices(); const { getHref } = useLink(); - const showAddButton = useConfig().internal?.fleetServerStandalone !== true; return ( <> @@ -59,21 +58,17 @@ export const FleetServerHostsSection: React.FunctionComponent - {showAddButton && ( - <> - - - - - - )} + + + + ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/index.tsx index a50eec2eed7f5c..809639ecae6924 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/index.tsx @@ -9,7 +9,6 @@ import React from 'react'; import { EuiSpacer } from '@elastic/eui'; import type { Output, DownloadSource, FleetServerHost, FleetProxy } from '../../../../types'; -import { useConfig } from '../../../../hooks'; import { FleetServerHostsSection } from './fleet_server_hosts_section'; import { OutputSection } from './output_section'; @@ -37,8 +36,6 @@ export const SettingsPage: React.FunctionComponent = ({ deleteDownloadSource, deleteFleetProxy, }) => { - const showProxySection = useConfig().internal?.disableProxies !== true; - return ( <> @@ -53,12 +50,8 @@ export const SettingsPage: React.FunctionComponent = ({ downloadSources={downloadSources} deleteDownloadSource={deleteDownloadSource} /> - {showProxySection && ( - <> - - - - )} + + ); }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/index.tsx index 93f541d2ffd8b6..2fc729cb76c822 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/index.tsx @@ -17,10 +17,15 @@ import { useGetFleetServerHosts, useFlyoutContext, useGetFleetProxies, + useStartServices, } from '../../hooks'; import { FLEET_ROUTING_PATHS, pagePathGetters } from '../../constants'; import { DefaultLayout } from '../../layouts'; import { Loading } from '../../components'; +import { + SERVERLESS_DEFAULT_FLEET_SERVER_HOST_ID, + SERVERLESS_DEFAULT_OUTPUT_ID, +} from '../../../../../common/constants'; import { FleetServerFlyout } from '../../components'; @@ -78,6 +83,8 @@ export const SettingsApp = withConfirmModalProvider(() => { history, ]); + const { cloud } = useStartServices(); + if ( (outputs.isLoading && outputs.isInitialRequest) || !outputItems || @@ -120,12 +127,26 @@ export const SettingsApp = withConfirmModalProvider(() => { - + {cloud?.isServerlessEnabled ? ( + o.id === SERVERLESS_DEFAULT_FLEET_SERVER_HOST_ID + )} + /> + ) : ( + + )} - + o.id === SERVERLESS_DEFAULT_OUTPUT_ID)} + /> @@ -161,6 +182,9 @@ export const SettingsApp = withConfirmModalProvider(() => { proxies={proxies.data?.items ?? []} onClose={onCloseCallback} output={output} + defaultOuput={outputs.data?.items.find( + (o) => o.id === SERVERLESS_DEFAULT_OUTPUT_ID + )} /> ); diff --git a/x-pack/plugins/fleet/server/config.ts b/x-pack/plugins/fleet/server/config.ts index a47058a80c8287..d339642492bb91 100644 --- a/x-pack/plugins/fleet/server/config.ts +++ b/x-pack/plugins/fleet/server/config.ts @@ -39,7 +39,6 @@ export const config: PluginConfigDescriptor = { }, internal: { fleetServerStandalone: true, - disableProxies: true, activeAgentsSoftLimit: true, onlyAllowAgentUpgradeToKnownVersions: true, }, @@ -184,9 +183,6 @@ export const config: PluginConfigDescriptor = { disableILMPolicies: schema.boolean({ defaultValue: false, }), - disableProxies: schema.boolean({ - defaultValue: false, - }), fleetServerStandalone: schema.boolean({ defaultValue: false, }), diff --git a/x-pack/plugins/fleet/server/constants/index.ts b/x-pack/plugins/fleet/server/constants/index.ts index 2a608b927f58cf..e134c0a47a82eb 100644 --- a/x-pack/plugins/fleet/server/constants/index.ts +++ b/x-pack/plugins/fleet/server/constants/index.ts @@ -51,6 +51,7 @@ export { // Defaults DEFAULT_OUTPUT, DEFAULT_OUTPUT_ID, + SERVERLESS_DEFAULT_OUTPUT_ID, PACKAGE_POLICY_DEFAULT_INDEX_PRIVILEGES, AGENT_POLICY_DEFAULT_MONITORING_DATASETS, // Fleet Server index @@ -71,6 +72,7 @@ export { // Fleet server host DEFAULT_FLEET_SERVER_HOST_ID, FLEET_SERVER_HOST_SAVED_OBJECT_TYPE, + SERVERLESS_DEFAULT_FLEET_SERVER_HOST_ID, // Proxy FLEET_PROXY_SAVED_OBJECT_TYPE, // Authz diff --git a/x-pack/plugins/fleet/server/routes/fleet_proxies/handler.ts b/x-pack/plugins/fleet/server/routes/fleet_proxies/handler.ts index 333f6d50bb8801..b636ebb53a9eb5 100644 --- a/x-pack/plugins/fleet/server/routes/fleet_proxies/handler.ts +++ b/x-pack/plugins/fleet/server/routes/fleet_proxies/handler.ts @@ -22,7 +22,7 @@ import { updateFleetProxy, getFleetProxyRelatedSavedObjects, } from '../../services/fleet_proxies'; -import { defaultFleetErrorHandler, FleetProxyUnauthorizedError } from '../../errors'; +import { defaultFleetErrorHandler } from '../../errors'; import type { GetOneFleetProxyRequestSchema, PostFleetProxyRequestSchema, @@ -31,7 +31,7 @@ import type { Output, DownloadSource, } from '../../types'; -import { agentPolicyService, appContextService } from '../../services'; +import { agentPolicyService } from '../../services'; async function bumpRelatedPolicies( soClient: SavedObjectsClientContract, @@ -81,12 +81,6 @@ async function bumpRelatedPolicies( } } -function checkProxiesAvailable() { - if (appContextService.getConfig()?.internal?.disableProxies) { - throw new FleetProxyUnauthorizedError('Proxies write APIs are disabled'); - } -} - export const postFleetProxyHandler: RequestHandler< undefined, undefined, @@ -95,7 +89,6 @@ export const postFleetProxyHandler: RequestHandler< const coreContext = await context.core; const soClient = coreContext.savedObjects.client; try { - checkProxiesAvailable(); const { id, ...data } = request.body; const proxy = await createFleetProxy(soClient, { ...data, is_preconfigured: false }, { id }); @@ -115,7 +108,6 @@ export const putFleetProxyHandler: RequestHandler< TypeOf > = async (context, request, response) => { try { - checkProxiesAvailable(); const proxyId = request.params.itemId; const coreContext = await await context.core; const soClient = coreContext.savedObjects.client; @@ -167,7 +159,6 @@ export const deleteFleetProxyHandler: RequestHandler< TypeOf > = async (context, request, response) => { try { - checkProxiesAvailable(); const proxyId = request.params.itemId; const coreContext = await context.core; const soClient = coreContext.savedObjects.client; diff --git a/x-pack/plugins/fleet/server/routes/fleet_server_hosts/handler.test.ts b/x-pack/plugins/fleet/server/routes/fleet_server_hosts/handler.test.ts new file mode 100644 index 00000000000000..e65942a50c9345 --- /dev/null +++ b/x-pack/plugins/fleet/server/routes/fleet_server_hosts/handler.test.ts @@ -0,0 +1,134 @@ +/* + * 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 { SERVERLESS_DEFAULT_FLEET_SERVER_HOST_ID } from '../../constants'; +import { agentPolicyService, appContextService } from '../../services'; +import * as fleetServerService from '../../services/fleet_server_host'; + +import { postFleetServerHost, putFleetServerHostHandler } from './handler'; + +describe('fleet server hosts handler', () => { + const mockContext = { + core: Promise.resolve({ + savedObjects: {}, + elasticsearch: { + client: {}, + }, + }), + } as any; + const mockResponse = { + customError: jest.fn().mockImplementation((options) => options), + ok: jest.fn().mockImplementation((options) => options), + }; + + beforeEach(() => { + jest.spyOn(appContextService, 'getLogger').mockReturnValue({ error: jest.fn() } as any); + jest + .spyOn(fleetServerService, 'createFleetServerHost') + .mockResolvedValue({ id: 'host1' } as any); + jest + .spyOn(fleetServerService, 'updateFleetServerHost') + .mockResolvedValue({ id: 'host1' } as any); + jest.spyOn(fleetServerService, 'listFleetServerHosts').mockResolvedValue({ + items: [ + { id: SERVERLESS_DEFAULT_FLEET_SERVER_HOST_ID, host_urls: ['http://elasticsearch:9200'] }, + ] as any, + total: 1, + page: 1, + perPage: 1, + }); + jest + .spyOn(agentPolicyService, 'bumpAllAgentPoliciesForFleetServerHosts') + .mockResolvedValue({} as any); + }); + + it('should return error on post in serverless if host url is different from default', async () => { + jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isServerlessEnabled: true } as any); + + const res = await postFleetServerHost( + mockContext, + { body: { id: 'host1', host_urls: ['http://localhost:8080'] } } as any, + mockResponse as any + ); + + expect(res).toEqual({ + body: { + message: 'Fleet server host must have default URL in serverless: http://elasticsearch:9200', + }, + statusCode: 403, + }); + }); + + it('should return ok on post in serverless if host url is same as default', async () => { + jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isServerlessEnabled: true } as any); + + const res = await postFleetServerHost( + mockContext, + { body: { id: 'host1', host_urls: ['http://elasticsearch:9200'] } } as any, + mockResponse as any + ); + + expect(res).toEqual({ body: { item: { id: 'host1' } } }); + }); + + it('should return ok on post in stateful if host url is different from default', async () => { + jest + .spyOn(appContextService, 'getCloud') + .mockReturnValue({ isServerlessEnabled: false } as any); + + const res = await postFleetServerHost( + mockContext, + { body: { id: 'host1', host_urls: ['http://localhost:8080'] } } as any, + mockResponse as any + ); + + expect(res).toEqual({ body: { item: { id: 'host1' } } }); + }); + + it('should return error on put in serverless if host url is different from default', async () => { + jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isServerlessEnabled: true } as any); + + const res = await putFleetServerHostHandler( + mockContext, + { body: { host_urls: ['http://localhost:8080'] }, params: { outputId: 'host1' } } as any, + mockResponse as any + ); + + expect(res).toEqual({ + body: { + message: 'Fleet server host must have default URL in serverless: http://elasticsearch:9200', + }, + statusCode: 403, + }); + }); + + it('should return ok on put in serverless if host url is same as default', async () => { + jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isServerlessEnabled: true } as any); + + const res = await putFleetServerHostHandler( + mockContext, + { body: { host_urls: ['http://elasticsearch:9200'] }, params: { outputId: 'host1' } } as any, + mockResponse as any + ); + + expect(res).toEqual({ body: { item: { id: 'host1' } } }); + }); + + // it('should return ok on put in stateful if host url is different from default', async () => { + // jest + // .spyOn(appContextService, 'getCloud') + // .mockReturnValue({ isServerlessEnabled: false } as any); + + // const res = await putFleetServerHostHandler( + // mockContext, + // { body: { host_urls: ['http://localhost:8080'] }, params: { outputId: 'host1' } } as any, + // mockResponse as any + // ); + + // expect(res).toEqual({ body: { item: { id: 'host1' } } }); + // }); +}); diff --git a/x-pack/plugins/fleet/server/routes/fleet_server_hosts/handler.ts b/x-pack/plugins/fleet/server/routes/fleet_server_hosts/handler.ts index 4ba6143c45b63a..eddce8df7c3e0b 100644 --- a/x-pack/plugins/fleet/server/routes/fleet_server_hosts/handler.ts +++ b/x-pack/plugins/fleet/server/routes/fleet_server_hosts/handler.ts @@ -6,8 +6,11 @@ */ import type { TypeOf } from '@kbn/config-schema'; -import type { RequestHandler } from '@kbn/core/server'; +import type { RequestHandler, SavedObjectsClientContract } from '@kbn/core/server'; import { SavedObjectsErrorHelpers } from '@kbn/core/server'; +import { isEqual } from 'lodash'; + +import { SERVERLESS_DEFAULT_FLEET_SERVER_HOST_ID } from '../../constants'; import { defaultFleetErrorHandler, FleetServerHostUnauthorizedError } from '../../errors'; import { agentPolicyService, appContextService } from '../../services'; @@ -24,13 +27,36 @@ import type { PutFleetServerHostRequestSchema, } from '../../types'; -function checkFleetServerHostsWriteAPIsAllowed() { - const config = appContextService.getConfig(); - if (config?.internal?.fleetServerStandalone) { - throw new FleetServerHostUnauthorizedError('Fleet server host write APIs are disabled'); +async function checkFleetServerHostsWriteAPIsAllowed( + soClient: SavedObjectsClientContract, + hostUrls: string[] +) { + const cloudSetup = appContextService.getCloud(); + if (!cloudSetup?.isServerlessEnabled) { + return; + } + + const defaultFleetServerHost = await getDefaultFleetServerHost(soClient); + if ( + defaultFleetServerHost === undefined || + !isEqual(hostUrls, defaultFleetServerHost.host_urls) + ) { + throw new FleetServerHostUnauthorizedError( + `Fleet server host must have default URL in serverless${ + defaultFleetServerHost ? ': ' + defaultFleetServerHost.host_urls : '' + }` + ); } } +async function getDefaultFleetServerHost(soClient: SavedObjectsClientContract) { + const res = await listFleetServerHosts(soClient); + const fleetServerHosts = res.items; + return fleetServerHosts.find( + (fleetServerHost) => fleetServerHost.id === SERVERLESS_DEFAULT_FLEET_SERVER_HOST_ID + ); +} + export const postFleetServerHost: RequestHandler< undefined, undefined, @@ -41,7 +67,8 @@ export const postFleetServerHost: RequestHandler< const esClient = coreContext.elasticsearch.client.asInternalUser; try { - checkFleetServerHostsWriteAPIsAllowed(); + // In serverless, allow create fleet server host if host url is same as default. + await checkFleetServerHostsWriteAPIsAllowed(soClient, request.body.host_urls); const { id, ...data } = request.body; const FleetServerHost = await createFleetServerHost( @@ -89,11 +116,10 @@ export const deleteFleetServerHostHandler: RequestHandler< TypeOf > = async (context, request, response) => { try { - checkFleetServerHostsWriteAPIsAllowed(); - const coreContext = await context.core; const soClient = coreContext.savedObjects.client; const esClient = coreContext.elasticsearch.client.asInternalUser; + await deleteFleetServerHost(soClient, esClient, request.params.itemId); const body = { id: request.params.itemId, @@ -117,12 +143,15 @@ export const putFleetServerHostHandler: RequestHandler< TypeOf > = async (context, request, response) => { try { - checkFleetServerHostsWriteAPIsAllowed(); - const coreContext = await await context.core; const esClient = coreContext.elasticsearch.client.asInternalUser; const soClient = coreContext.savedObjects.client; + // In serverless, allow update fleet server host if host url is same as default. + if (request.body.host_urls) { + await checkFleetServerHostsWriteAPIsAllowed(soClient, request.body.host_urls); + } + const item = await updateFleetServerHost(soClient, request.params.itemId, request.body); const body = { item, diff --git a/x-pack/plugins/fleet/server/routes/output/handler.test.ts b/x-pack/plugins/fleet/server/routes/output/handler.test.ts index 5cf3b544e1553b..be1c162d7056b6 100644 --- a/x-pack/plugins/fleet/server/routes/output/handler.test.ts +++ b/x-pack/plugins/fleet/server/routes/output/handler.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { SERVERLESS_DEFAULT_OUTPUT_ID } from '../../constants'; import { agentPolicyService, appContextService, outputService } from '../../services'; import { postOutputHandler, putOutputHandler } from './handler'; @@ -27,7 +28,13 @@ describe('output handler', () => { jest.spyOn(appContextService, 'getLogger').mockReturnValue({ error: jest.fn() } as any); jest.spyOn(outputService, 'create').mockResolvedValue({ id: 'output1' } as any); jest.spyOn(outputService, 'update').mockResolvedValue({ id: 'output1' } as any); - jest.spyOn(outputService, 'get').mockResolvedValue({ id: 'output1' } as any); + jest.spyOn(outputService, 'get').mockImplementation((_, id: string) => { + if (id === SERVERLESS_DEFAULT_OUTPUT_ID) { + return { hosts: ['http://elasticsearch:9200'] } as any; + } else { + return { id: 'output1' } as any; + } + }); jest.spyOn(agentPolicyService, 'bumpAllAgentPoliciesForOutput').mockResolvedValue({} as any); }); @@ -65,7 +72,7 @@ describe('output handler', () => { const res = await putOutputHandler( mockContext, - { body: { id: 'output1', type: 'remote_elasticsearch' } } as any, + { body: { type: 'remote_elasticsearch' }, params: { outputId: 'output1' } } as any, mockResponse as any ); @@ -89,6 +96,105 @@ describe('output handler', () => { expect(res).toEqual({ body: { item: { id: 'output1' } } }); }); + it('should return error on post elasticsearch output in serverless if host url is different from default', async () => { + jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isServerlessEnabled: true } as any); + + const res = await postOutputHandler( + mockContext, + { body: { id: 'output1', type: 'elasticsearch', hosts: ['http://localhost:8080'] } } as any, + mockResponse as any + ); + + expect(res).toEqual({ + body: { + message: + 'Elasticsearch output host must have default URL in serverless: http://elasticsearch:9200', + }, + statusCode: 400, + }); + }); + + it('should return ok on post elasticsearch output in serverless if host url is same as default', async () => { + jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isServerlessEnabled: true } as any); + + const res = await postOutputHandler( + mockContext, + { + body: { id: 'output1', type: 'elasticsearch', hosts: ['http://elasticsearch:9200'] }, + } as any, + mockResponse as any + ); + + expect(res).toEqual({ body: { item: { id: 'output1' } } }); + }); + + it('should return ok on post elasticsearch output in stateful if host url is different from default', async () => { + jest + .spyOn(appContextService, 'getCloud') + .mockReturnValue({ isServerlessEnabled: false } as any); + + const res = await postOutputHandler( + mockContext, + { body: { id: 'output1', type: 'elasticsearch', hosts: ['http://localhost:8080'] } } as any, + mockResponse as any + ); + + expect(res).toEqual({ body: { item: { id: 'output1' } } }); + }); + + it('should return error on put elasticsearch output in serverless if host url is different from default', async () => { + jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isServerlessEnabled: true } as any); + + const res = await putOutputHandler( + mockContext, + { + body: { type: 'elasticsearch', hosts: ['http://localhost:8080'] }, + params: { outputId: 'output1' }, + } as any, + mockResponse as any + ); + + expect(res).toEqual({ + body: { + message: + 'Elasticsearch output host must have default URL in serverless: http://elasticsearch:9200', + }, + statusCode: 400, + }); + }); + + it('should return ok on put elasticsearch output in serverless if host url is same as default', async () => { + jest.spyOn(appContextService, 'getCloud').mockReturnValue({ isServerlessEnabled: true } as any); + + const res = await putOutputHandler( + mockContext, + { + body: { type: 'elasticsearch', hosts: ['http://elasticsearch:9200'] }, + params: { outputId: 'output1' }, + } as any, + mockResponse as any + ); + + expect(res).toEqual({ body: { item: { id: 'output1' } } }); + }); + + it('should return ok on put elasticsearch output in stateful if host url is different from default', async () => { + jest + .spyOn(appContextService, 'getCloud') + .mockReturnValue({ isServerlessEnabled: false } as any); + + const res = await putOutputHandler( + mockContext, + { + body: { type: 'elasticsearch', hosts: ['http://localhost:8080'] }, + params: { outputId: 'output1' }, + } as any, + mockResponse as any + ); + + expect(res).toEqual({ body: { item: { id: 'output1' } } }); + }); + it('should return error if both service_token and secrets.service_token is provided for remote_elasticsearch output', async () => { jest .spyOn(appContextService, 'getCloud') diff --git a/x-pack/plugins/fleet/server/routes/output/handler.ts b/x-pack/plugins/fleet/server/routes/output/handler.ts index a63838e7a2063f..237ff2986703c5 100644 --- a/x-pack/plugins/fleet/server/routes/output/handler.ts +++ b/x-pack/plugins/fleet/server/routes/output/handler.ts @@ -5,14 +5,14 @@ * 2.0. */ -import type { RequestHandler } from '@kbn/core/server'; +import type { RequestHandler, SavedObjectsClientContract } from '@kbn/core/server'; import type { TypeOf } from '@kbn/config-schema'; import Boom from '@hapi/boom'; -import type { ValueOf } from '@elastic/eui'; +import { isEqual } from 'lodash'; -import { outputType } from '../../../common/constants'; +import { SERVERLESS_DEFAULT_OUTPUT_ID, outputType } from '../../../common/constants'; import type { DeleteOutputRequestSchema, @@ -26,7 +26,6 @@ import type { GetOneOutputResponse, GetOutputsResponse, Output, - OutputType, PostLogstashApiKeyResponse, } from '../../../common/types'; import { outputService } from '../../services/output'; @@ -105,7 +104,7 @@ export const putOutputHandler: RequestHandler< const esClient = coreContext.elasticsearch.client.asInternalUser; const outputUpdate = request.body; try { - validateOutputServerless(outputUpdate.type); + await validateOutputServerless(outputUpdate, soClient, request.params.outputId); ensureNoDuplicateSecrets(outputUpdate); await outputService.update(soClient, esClient, request.params.outputId, outputUpdate); const output = await outputService.get(soClient, request.params.outputId); @@ -141,7 +140,7 @@ export const postOutputHandler: RequestHandler< const esClient = coreContext.elasticsearch.client.asInternalUser; try { const { id, ...newOutput } = request.body; - validateOutputServerless(newOutput.type); + await validateOutputServerless(newOutput, soClient); ensureNoDuplicateSecrets(newOutput); const output = await outputService.create(soClient, esClient, newOutput, { id }); if (output.is_default || output.is_default_monitoring) { @@ -158,11 +157,30 @@ export const postOutputHandler: RequestHandler< } }; -function validateOutputServerless(type?: ValueOf): void { +async function validateOutputServerless( + output: Partial, + soClient: SavedObjectsClientContract, + outputId?: string +): Promise { const cloudSetup = appContextService.getCloud(); - if (cloudSetup?.isServerlessEnabled && type === outputType.RemoteElasticsearch) { + if (!cloudSetup?.isServerlessEnabled) { + return; + } + if (output.type === outputType.RemoteElasticsearch) { throw Boom.badRequest('Output type remote_elasticsearch not supported in serverless'); } + // Elasticsearch outputs must have the default host URL in serverless. + const defaultOutput = await outputService.get(soClient, SERVERLESS_DEFAULT_OUTPUT_ID); + let originalOutput; + if (outputId) { + originalOutput = await outputService.get(soClient, outputId); + } + const type = output.type || originalOutput?.type; + if (type === outputType.Elasticsearch && !isEqual(output.hosts, defaultOutput.hosts)) { + throw Boom.badRequest( + `Elasticsearch output host must have default URL in serverless: ${defaultOutput.hosts}` + ); + } } export const deleteOutputHandler: RequestHandler< diff --git a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts index 56e613d617b03d..7568dafe248ac4 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts @@ -140,7 +140,6 @@ describe('_installPackage', () => { createAppContextStartContractMock({ internal: { disableILMPolicies: true, - disableProxies: false, fleetServerStandalone: false, onlyAllowAgentUpgradeToKnownVersions: false, retrySetupOnBoot: false, @@ -201,7 +200,6 @@ describe('_installPackage', () => { appContextService.start( createAppContextStartContractMock({ internal: { - disableProxies: false, disableILMPolicies: false, fleetServerStandalone: false, onlyAllowAgentUpgradeToKnownVersions: false, @@ -279,7 +277,6 @@ describe('_installPackage', () => { createAppContextStartContractMock({ internal: { disableILMPolicies: true, - disableProxies: false, fleetServerStandalone: false, onlyAllowAgentUpgradeToKnownVersions: false, retrySetupOnBoot: false, @@ -394,7 +391,6 @@ describe('_installPackage', () => { createAppContextStartContractMock({ internal: { disableILMPolicies: false, - disableProxies: false, fleetServerStandalone: false, onlyAllowAgentUpgradeToKnownVersions: false, retrySetupOnBoot: false, diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/fleet/config.ts b/x-pack/test_serverless/api_integration/test_suites/observability/fleet/config.ts new file mode 100644 index 00000000000000..b3c3918c4dfd52 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/observability/fleet/config.ts @@ -0,0 +1,41 @@ +/* + * 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 { createTestConfig } from '../../../config.base'; +import { services } from '../apm_api_integration/common/services'; + +export default createTestConfig({ + serverlessProject: 'oblt', + testFiles: [require.resolve('./fleet')], + junit: { + reportName: 'Fleet Serverless Observability API Integration Tests', + }, + suiteTags: { exclude: ['skipSvlOblt'] }, + services, + + // include settings from project controller + // https://github.com/elastic/project-controller/blob/main/internal/project/observability/config/elasticsearch.yml + esServerArgs: ['xpack.ml.dfa.enabled=false', 'xpack.ml.nlp.enabled=false'], + + kbnServerArgs: [ + '--xpack.cloud.serverless.project_id=ftr_fake_project_id', + `--xpack.fleet.fleetServerHosts=[${JSON.stringify({ + id: 'default-fleet-server', + name: 'Default Fleet Server', + is_default: true, + host_urls: ['https://localhost:8220'], + })}]`, + `--xpack.fleet.outputs=[${JSON.stringify({ + id: 'es-default-output', + name: 'Default Output', + type: 'elasticsearch', + is_default: true, + is_default_monitoring: true, + hosts: ['https://localhost:9200'], + })}]`, + ], +}); diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/fleet/fleet.ts b/x-pack/test_serverless/api_integration/test_suites/observability/fleet/fleet.ts index 24f5e9cde91775..98866bc50f4316 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/fleet/fleet.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/fleet/fleet.ts @@ -13,40 +13,79 @@ export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); describe('fleet', function () { - it('rejects request to create a new fleet server hosts', async () => { + it('rejects request to create a new fleet server hosts if host url is different from default', async () => { const { body, status } = await supertest .post('/api/fleet/fleet_server_hosts') .set(svlCommonApi.getInternalRequestHeader()) .send({ name: 'test', - host_urls: ['https://localhost:8220'], + host_urls: ['https://localhost:8221'], }); // in a non-serverless environment this would succeed with a 200 expect(body).toEqual({ statusCode: 403, error: 'Forbidden', - message: 'Fleet server host write APIs are disabled', + message: 'Fleet server host must have default URL in serverless: https://localhost:8220', }); expect(status).toBe(403); }); - it('rejects request to create a new proxy', async () => { + it('accepts request to create a new fleet server hosts if host url is same as default', async () => { const { body, status } = await supertest - .post('/api/fleet/proxies') + .post('/api/fleet/fleet_server_hosts') .set(svlCommonApi.getInternalRequestHeader()) .send({ - name: 'test', - url: 'https://localhost:8220', + name: 'Test Fleet server host', + host_urls: ['https://localhost:8220'], + }); + + expect(body).toEqual({ + item: expect.objectContaining({ + name: 'Test Fleet server host', + host_urls: ['https://localhost:8220'], + }), + }); + expect(status).toBe(200); + }); + + it('rejects request to create a new elasticsearch output if host is different from default', async () => { + const { body, status } = await supertest + .post('/api/fleet/outputs') + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + name: 'Test output', + type: 'elasticsearch', + hosts: ['https://localhost:9201'], }); // in a non-serverless environment this would succeed with a 200 expect(body).toEqual({ - statusCode: 403, - error: 'Forbidden', - message: 'Proxies write APIs are disabled', + statusCode: 400, + error: 'Bad Request', + message: + 'Elasticsearch output host must have default URL in serverless: https://localhost:9200', }); - expect(status).toBe(403); + expect(status).toBe(400); + }); + + it('accepts request to create a new elasticsearch output if host url is same as default', async () => { + const { body, status } = await supertest + .post('/api/fleet/outputs') + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + name: 'Test output', + type: 'elasticsearch', + hosts: ['https://localhost:9200'], + }); + + expect(body).toEqual({ + item: expect.objectContaining({ + name: 'Test output', + hosts: ['https://localhost:9200'], + }), + }); + expect(status).toBe(200); }); }); } diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/index.ts b/x-pack/test_serverless/api_integration/test_suites/observability/index.ts index 74bf14d1b0c48b..765dd693d4dbce 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/index.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/index.ts @@ -11,7 +11,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { describe('Serverless observability API', function () { this.tags(['esGate']); - loadTestFile(require.resolve('./fleet/fleet')); loadTestFile(require.resolve('./telemetry/snapshot_telemetry')); loadTestFile(require.resolve('./telemetry/telemetry_config')); loadTestFile(require.resolve('./apm_api_integration/feature_flags.ts')); diff --git a/x-pack/test_serverless/api_integration/test_suites/security/fleet/config.ts b/x-pack/test_serverless/api_integration/test_suites/security/fleet/config.ts new file mode 100644 index 00000000000000..2805cfa7c607a1 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/security/fleet/config.ts @@ -0,0 +1,39 @@ +/* + * 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 { createTestConfig } from '../../../config.base'; + +export default createTestConfig({ + serverlessProject: 'security', + testFiles: [require.resolve('./fleet')], + junit: { + reportName: 'Fleet Serverless Security API Integration Tests', + }, + suiteTags: { exclude: ['skipSvlSec'] }, + + // include settings from project controller + // https://github.com/elastic/project-controller/blob/main/internal/project/security/config/elasticsearch.yml + esServerArgs: ['xpack.ml.nlp.enabled=false'], + + kbnServerArgs: [ + '--xpack.cloud.serverless.project_id=ftr_fake_project_id', + `--xpack.fleet.fleetServerHosts=[${JSON.stringify({ + id: 'default-fleet-server', + name: 'Default Fleet Server', + is_default: true, + host_urls: ['https://localhost:8220'], + })}]`, + `--xpack.fleet.outputs=[${JSON.stringify({ + id: 'es-default-output', + name: 'Default Output', + type: 'elasticsearch', + is_default: true, + is_default_monitoring: true, + hosts: ['https://localhost:9200'], + })}]`, + ], +}); diff --git a/x-pack/test_serverless/api_integration/test_suites/security/fleet/fleet.ts b/x-pack/test_serverless/api_integration/test_suites/security/fleet/fleet.ts index 24f5e9cde91775..98866bc50f4316 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/fleet/fleet.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/fleet/fleet.ts @@ -13,40 +13,79 @@ export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); describe('fleet', function () { - it('rejects request to create a new fleet server hosts', async () => { + it('rejects request to create a new fleet server hosts if host url is different from default', async () => { const { body, status } = await supertest .post('/api/fleet/fleet_server_hosts') .set(svlCommonApi.getInternalRequestHeader()) .send({ name: 'test', - host_urls: ['https://localhost:8220'], + host_urls: ['https://localhost:8221'], }); // in a non-serverless environment this would succeed with a 200 expect(body).toEqual({ statusCode: 403, error: 'Forbidden', - message: 'Fleet server host write APIs are disabled', + message: 'Fleet server host must have default URL in serverless: https://localhost:8220', }); expect(status).toBe(403); }); - it('rejects request to create a new proxy', async () => { + it('accepts request to create a new fleet server hosts if host url is same as default', async () => { const { body, status } = await supertest - .post('/api/fleet/proxies') + .post('/api/fleet/fleet_server_hosts') .set(svlCommonApi.getInternalRequestHeader()) .send({ - name: 'test', - url: 'https://localhost:8220', + name: 'Test Fleet server host', + host_urls: ['https://localhost:8220'], + }); + + expect(body).toEqual({ + item: expect.objectContaining({ + name: 'Test Fleet server host', + host_urls: ['https://localhost:8220'], + }), + }); + expect(status).toBe(200); + }); + + it('rejects request to create a new elasticsearch output if host is different from default', async () => { + const { body, status } = await supertest + .post('/api/fleet/outputs') + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + name: 'Test output', + type: 'elasticsearch', + hosts: ['https://localhost:9201'], }); // in a non-serverless environment this would succeed with a 200 expect(body).toEqual({ - statusCode: 403, - error: 'Forbidden', - message: 'Proxies write APIs are disabled', + statusCode: 400, + error: 'Bad Request', + message: + 'Elasticsearch output host must have default URL in serverless: https://localhost:9200', }); - expect(status).toBe(403); + expect(status).toBe(400); + }); + + it('accepts request to create a new elasticsearch output if host url is same as default', async () => { + const { body, status } = await supertest + .post('/api/fleet/outputs') + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + name: 'Test output', + type: 'elasticsearch', + hosts: ['https://localhost:9200'], + }); + + expect(body).toEqual({ + item: expect.objectContaining({ + name: 'Test output', + hosts: ['https://localhost:9200'], + }), + }); + expect(status).toBe(200); }); }); } diff --git a/x-pack/test_serverless/api_integration/test_suites/security/index.ts b/x-pack/test_serverless/api_integration/test_suites/security/index.ts index e439cf8b76e8b1..e6cf273c295d5d 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/index.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/index.ts @@ -13,7 +13,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./telemetry/snapshot_telemetry')); loadTestFile(require.resolve('./telemetry/telemetry_config')); - loadTestFile(require.resolve('./fleet/fleet')); loadTestFile(require.resolve('./cases')); loadTestFile(require.resolve('./cloud_security_posture')); });