diff --git a/x-pack/plugins/fleet/common/constants/routes.ts b/x-pack/plugins/fleet/common/constants/routes.ts index 5aeba4bc3881..377cb8d8bd87 100644 --- a/x-pack/plugins/fleet/common/constants/routes.ts +++ b/x-pack/plugins/fleet/common/constants/routes.ts @@ -76,6 +76,7 @@ export const SETTINGS_API_ROUTES = { // App API routes export const APP_API_ROUTES = { CHECK_PERMISSIONS_PATTERN: `${API_ROOT}/check-permissions`, + GENERATE_SERVICE_TOKEN_PATTERN: `${API_ROOT}/service-tokens`, }; // Agent API routes diff --git a/x-pack/plugins/fleet/common/services/routes.ts b/x-pack/plugins/fleet/common/services/routes.ts index e1b3791d9cbb..6156decf8641 100644 --- a/x-pack/plugins/fleet/common/services/routes.ts +++ b/x-pack/plugins/fleet/common/services/routes.ts @@ -164,6 +164,7 @@ export const settingsRoutesService = { export const appRoutesService = { getCheckPermissionsPath: () => APP_API_ROUTES.CHECK_PERMISSIONS_PATTERN, + getRegenerateServiceTokenPath: () => APP_API_ROUTES.GENERATE_SERVICE_TOKEN_PATTERN, }; export const enrollmentAPIKeyRouteService = { diff --git a/x-pack/plugins/fleet/common/types/rest_spec/app.ts b/x-pack/plugins/fleet/common/types/rest_spec/app.ts index 3e54cf04d753..a742c387c14a 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/app.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/app.ts @@ -9,3 +9,8 @@ export interface CheckPermissionsResponse { error?: 'MISSING_SECURITY' | 'MISSING_SUPERUSER_ROLE'; success: boolean; } + +export interface GenerateServiceTokenResponse { + name: string; + value: string; +} diff --git a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_request/app.ts b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_request/app.ts index bd690a4b53e0..c84dd0fd15b4 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_request/app.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_request/app.ts @@ -6,7 +6,7 @@ */ import { appRoutesService } from '../../services'; -import type { CheckPermissionsResponse } from '../../types'; +import type { CheckPermissionsResponse, GenerateServiceTokenResponse } from '../../types'; import { sendRequest } from './use_request'; @@ -16,3 +16,10 @@ export const sendGetPermissionsCheck = () => { method: 'get', }); }; + +export const sendGenerateServiceToken = () => { + return sendRequest({ + path: appRoutesService.getRegenerateServiceTokenPath(), + method: 'post', + }); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/fleet_server_requirement_page.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/fleet_server_requirement_page.tsx index e5f3cdbcfba9..463fdb4c62ca 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/fleet_server_requirement_page.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/fleet_server_requirement_page.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useState, useMemo, useCallback } from 'react'; import { EuiButton, EuiFlexGroup, @@ -16,62 +16,229 @@ import { EuiText, EuiLink, EuiEmptyPrompt, + EuiSteps, + EuiCodeBlock, + EuiCallOut, + EuiSelect, } from '@elastic/eui'; import styled from 'styled-components'; -import { FormattedMessage } from 'react-intl'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; -import { useStartServices } from '../../../hooks'; +import { DownloadStep } from '../components/agent_enrollment_flyout/steps'; +import { useStartServices, useGetOutputs, sendGenerateServiceToken } from '../../../hooks'; + +const FlexItemWithMinWidth = styled(EuiFlexItem)` + min-width: 0px; + max-width: 100%; +`; export const ContentWrapper = styled(EuiFlexGroup)` height: 100%; + margin: 0 auto; + max-width: 800px; `; -function renderOnPremInstructions() { +// Otherwise the copy button is over the text +const CommandCode = styled.pre({ + overflow: 'scroll', +}); + +type PLATFORM_TYPE = 'linux-mac' | 'windows' | 'rpm-deb'; +const PLATFORM_OPTIONS: Array<{ text: string; value: PLATFORM_TYPE }> = [ + { text: 'Linux / macOS', value: 'linux-mac' }, + { text: 'Windows', value: 'windows' }, + { text: 'RPM / DEB', value: 'rpm-deb' }, +]; + +const OnPremInstructions: React.FC = () => { + const outputsRequest = useGetOutputs(); + const { notifications } = useStartServices(); + const [serviceToken, setServiceToken] = useState(); + const [isLoadingServiceToken, setIsLoadingServiceToken] = useState(false); + const [platform, setPlatform] = useState('linux-mac'); + + const output = outputsRequest.data?.items?.[0]; + const esHost = output?.hosts?.[0]; + + const installCommand = useMemo((): string => { + if (!serviceToken || !esHost) { + return ''; + } + switch (platform) { + case 'linux-mac': + return `sudo ./elastic-agent install -f --fleet-server-es=${esHost} --fleet-server-service-token=${serviceToken}`; + case 'windows': + return `.\\elastic-agent.exe install --fleet-server-es=${esHost} --fleet-server-service-token=${serviceToken}`; + case 'rpm-deb': + return `sudo elastic-agent enroll -f --fleet-server-es=${esHost} --fleet-server-service-token=${serviceToken}`; + default: + return ''; + } + }, [serviceToken, esHost, platform]); + + const getServiceToken = useCallback(async () => { + setIsLoadingServiceToken(true); + try { + const { data } = await sendGenerateServiceToken(); + if (data?.value) { + setServiceToken(data?.value); + } + } catch (err) { + notifications.toasts.addError(err, { + title: i18n.translate('xpack.fleet.fleetServerSetup.errorGeneratingTokenTitleText', { + defaultMessage: 'Error generating token', + }), + }); + } + + setIsLoadingServiceToken(false); + }, [notifications]); + return ( - - - - - } - body={ + + + +

- } - actions={ - - - - } +

+ + + + + ), + }} + /> +
+ + + + + + + {!serviceToken ? ( + + + { + getServiceToken(); + }} + > + + + + + ) : ( + <> + + + + + + + + + + + + + {serviceToken} + + + + + )} + + ), + }, + { + title: i18n.translate('xpack.fleet.fleetServerSetup.stepInstallAgentTitle', { + defaultMessage: 'Install the Elastic Agent as a Fleet Server', + }), + status: !serviceToken ? 'disabled' : undefined, + children: serviceToken ? ( + <> + + + + + + + + } + options={PLATFORM_OPTIONS} + value={platform} + onChange={(e) => setPlatform(e.target.value as PLATFORM_TYPE)} + aria-label={i18n.translate( + 'xpack.fleet.fleetServerSetup.platformSelectAriaLabel', + { + defaultMessage: 'Platform', + } + )} + /> + + + {installCommand} + + + ) : null, + }, + ]} />
); -} +}; -function renderCloudInstructions(deploymentUrl: string) { +const CloudInstructions: React.FC<{ deploymentUrl: string }> = ({ deploymentUrl }) => { return ( ); -} +}; export const FleetServerRequirementPage = () => { const startService = useStartServices(); @@ -134,11 +301,16 @@ export const FleetServerRequirementPage = () => { return ( <> - + + + {deploymentUrl ? ( + + ) : ( + + )} + - {deploymentUrl ? renderCloudInstructions(deploymentUrl) : renderOnPremInstructions()} - - + diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/index.tsx index d3c6ec114ee0..0ad1706e5273 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_enrollment_flyout/index.tsx @@ -129,12 +129,12 @@ export const AgentEnrollmentFlyout: React.FunctionComponent = ({ ) : undefined } > - {fleetServerHosts.length === 0 ? null : mode === 'managed' ? ( + {fleetServerHosts.length === 0 && mode === 'managed' ? null : mode === 'managed' ? ( ) : ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/types/index.ts b/x-pack/plugins/fleet/public/applications/fleet/types/index.ts index 89aa5ad1add3..0d85bfcdb6af 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/types/index.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/types/index.ts @@ -88,6 +88,7 @@ export { PutSettingsResponse, // API schemas - app CheckPermissionsResponse, + GenerateServiceTokenResponse, // EPM types AssetReference, AssetsGroupedByServiceByType, diff --git a/x-pack/plugins/fleet/server/errors/index.ts b/x-pack/plugins/fleet/server/errors/index.ts index 6738e078e8b7..793a349f730f 100644 --- a/x-pack/plugins/fleet/server/errors/index.ts +++ b/x-pack/plugins/fleet/server/errors/index.ts @@ -47,6 +47,7 @@ export class AgentUnenrollmentError extends IngestManagerError {} export class AgentPolicyDeletionError extends IngestManagerError {} export class FleetSetupError extends IngestManagerError {} +export class GenerateServiceTokenError extends IngestManagerError {} export class ArtifactsClientError extends IngestManagerError {} export class ArtifactsClientAccessDeniedError extends IngestManagerError { diff --git a/x-pack/plugins/fleet/server/routes/app/index.ts b/x-pack/plugins/fleet/server/routes/app/index.ts index ba7c649c4fa5..f2fc6302c8ce 100644 --- a/x-pack/plugins/fleet/server/routes/app/index.ts +++ b/x-pack/plugins/fleet/server/routes/app/index.ts @@ -7,9 +7,10 @@ import type { IRouter, RequestHandler } from 'src/core/server'; -import { APP_API_ROUTES } from '../../constants'; +import { PLUGIN_ID, APP_API_ROUTES } from '../../constants'; import { appContextService } from '../../services'; -import type { CheckPermissionsResponse } from '../../../common'; +import type { CheckPermissionsResponse, GenerateServiceTokenResponse } from '../../../common'; +import { defaultIngestErrorHandler, GenerateServiceTokenError } from '../../errors'; export const getCheckPermissionsHandler: RequestHandler = async (context, request, response) => { const body: CheckPermissionsResponse = { success: true }; @@ -35,6 +36,29 @@ export const getCheckPermissionsHandler: RequestHandler = async (context, reques } }; +export const generateServiceTokenHandler: RequestHandler = async (context, request, response) => { + const esClient = context.core.elasticsearch.client.asCurrentUser; + try { + const { body: tokenResponse } = await esClient.transport.request({ + method: 'POST', + path: `_security/service/elastic/fleet-server/credential/token/token-${Date.now()}`, + }); + + if (tokenResponse.created && tokenResponse.token) { + const body: GenerateServiceTokenResponse = tokenResponse.token; + return response.ok({ + body, + }); + } else { + const error = new GenerateServiceTokenError('Unable to generate service token'); + return defaultIngestErrorHandler({ error, response }); + } + } catch (e) { + const error = new GenerateServiceTokenError(e); + return defaultIngestErrorHandler({ error, response }); + } +}; + export const registerRoutes = (router: IRouter) => { router.get( { @@ -44,4 +68,13 @@ export const registerRoutes = (router: IRouter) => { }, getCheckPermissionsHandler ); + + router.post( + { + path: APP_API_ROUTES.GENERATE_SERVICE_TOKEN_PATTERN, + validate: {}, + options: { tags: [`access:${PLUGIN_ID}-all`] }, + }, + generateServiceTokenHandler + ); }; diff --git a/x-pack/test/fleet_api_integration/apis/index.js b/x-pack/test/fleet_api_integration/apis/index.js index 722d15751564..4d2bf1d74a49 100644 --- a/x-pack/test/fleet_api_integration/apis/index.js +++ b/x-pack/test/fleet_api_integration/apis/index.js @@ -43,5 +43,8 @@ export default function ({ loadTestFile }) { // Preconfiguration loadTestFile(require.resolve('./preconfiguration/index')); + + // Service tokens + loadTestFile(require.resolve('./service_tokens')); }); } diff --git a/x-pack/test/fleet_api_integration/apis/service_tokens.ts b/x-pack/test/fleet_api_integration/apis/service_tokens.ts new file mode 100644 index 000000000000..ddd4aed30f76 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/service_tokens.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../api_integration/ftr_provider_context'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const esClient = getService('es'); + + describe('fleet_service_tokens', async () => { + before(async () => { + await esArchiver.load('empty_kibana'); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + }); + + describe('POST /api/fleet/service-tokens', () => { + it('should create a valid service account token', async () => { + const { body: apiResponse } = await supertest + .post(`/api/fleet/service-tokens`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + + expect(apiResponse).have.property('name'); + expect(apiResponse).have.property('value'); + + const { body: tokensResponse } = await esClient.transport.request({ + method: 'GET', + path: `_security/service/elastic/fleet-server/credential`, + }); + + expect(tokensResponse.tokens).have.property(apiResponse.name); + }); + }); + }); +}