From 4a8791ba9ba86e908f305dd0f119f8f3bc414325 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Wed, 16 Dec 2020 14:49:31 -0500 Subject: [PATCH] [Fleet] Use fleet server indices for enrollment keys --- .../common/constants/enrollment_api_key.ts | 2 + x-pack/plugins/fleet/common/types/index.ts | 1 + .../common/types/models/enrollment_api_key.ts | 28 +++ .../plugins/fleet/server/constants/index.ts | 2 + x-pack/plugins/fleet/server/index.ts | 1 + .../routes/enrollment_api_key/handler.ts | 29 ++- .../fleet/server/routes/setup/handlers.ts | 2 +- .../services/api_keys/enrollment_api_key.ts | 166 +++----------- .../enrollment_api_key_fleet_server.ts | 205 ++++++++++++++++++ .../api_keys/enrollment_api_key_so.ts | 174 +++++++++++++++ x-pack/plugins/fleet/server/services/setup.ts | 5 +- x-pack/plugins/fleet/server/types/index.tsx | 2 + 12 files changed, 476 insertions(+), 141 deletions(-) create mode 100644 x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_fleet_server.ts create mode 100644 x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_so.ts diff --git a/x-pack/plugins/fleet/common/constants/enrollment_api_key.ts b/x-pack/plugins/fleet/common/constants/enrollment_api_key.ts index fd28b6632b15c..ce774f2212461 100644 --- a/x-pack/plugins/fleet/common/constants/enrollment_api_key.ts +++ b/x-pack/plugins/fleet/common/constants/enrollment_api_key.ts @@ -5,3 +5,5 @@ */ export const ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE = 'fleet-enrollment-api-keys'; + +export const ENROLLMENT_API_KEYS_INDEX = '.fleet-enrollment-api-keys'; diff --git a/x-pack/plugins/fleet/common/types/index.ts b/x-pack/plugins/fleet/common/types/index.ts index e0827ef7cf40f..b023052b1328d 100644 --- a/x-pack/plugins/fleet/common/types/index.ts +++ b/x-pack/plugins/fleet/common/types/index.ts @@ -11,6 +11,7 @@ export interface FleetConfigType { registryUrl?: string; registryProxyUrl?: string; agents: { + fleetServerEnabled: boolean; enabled: boolean; tlsCheckDisabled: boolean; pollingRequestTimeout: number; diff --git a/x-pack/plugins/fleet/common/types/models/enrollment_api_key.ts b/x-pack/plugins/fleet/common/types/models/enrollment_api_key.ts index f39076ce1027b..81dc6889f9946 100644 --- a/x-pack/plugins/fleet/common/types/models/enrollment_api_key.ts +++ b/x-pack/plugins/fleet/common/types/models/enrollment_api_key.ts @@ -15,3 +15,31 @@ export interface EnrollmentAPIKey { } export type EnrollmentAPIKeySOAttributes = Omit; + +// Generated + +/** + * An Elastic Agent enrollment API key + */ +export interface FleetServerEnrollmentAPIKey { + /** + * True when the key is active + */ + active?: boolean; + /** + * The unique identifier for the enrollment key, currently xid + */ + api_key_id: string; + /** + * Api key + */ + api_key: string; + /** + * Enrollment key name + */ + name?: string; + policy_id?: string; + expire_at?: string; + created_at?: string; + updated_at?: string; +} diff --git a/x-pack/plugins/fleet/server/constants/index.ts b/x-pack/plugins/fleet/server/constants/index.ts index b1d7318ff5107..8c0ae1eefeb7d 100644 --- a/x-pack/plugins/fleet/server/constants/index.ts +++ b/x-pack/plugins/fleet/server/constants/index.ts @@ -44,6 +44,8 @@ export { INDEX_PATTERN_SAVED_OBJECT_TYPE, ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, + // Fleet Server index + ENROLLMENT_API_KEYS_INDEX, // Defaults DEFAULT_AGENT_POLICY, DEFAULT_OUTPUT, diff --git a/x-pack/plugins/fleet/server/index.ts b/x-pack/plugins/fleet/server/index.ts index 1fe7013944fd7..672911ccf6fe0 100644 --- a/x-pack/plugins/fleet/server/index.ts +++ b/x-pack/plugins/fleet/server/index.ts @@ -37,6 +37,7 @@ export const config: PluginConfigDescriptor = { registryProxyUrl: schema.maybe(schema.uri({ scheme: ['http', 'https'] })), agents: schema.object({ enabled: schema.boolean({ defaultValue: true }), + fleetServerEnabled: schema.boolean({ defaultValue: false }), tlsCheckDisabled: schema.boolean({ defaultValue: false }), pollingRequestTimeout: schema.number({ defaultValue: AGENT_POLLING_REQUEST_TIMEOUT_MS, diff --git a/x-pack/plugins/fleet/server/routes/enrollment_api_key/handler.ts b/x-pack/plugins/fleet/server/routes/enrollment_api_key/handler.ts index afecd7bd7d828..4f54b4e155ea3 100644 --- a/x-pack/plugins/fleet/server/routes/enrollment_api_key/handler.ts +++ b/x-pack/plugins/fleet/server/routes/enrollment_api_key/handler.ts @@ -26,12 +26,18 @@ export const getEnrollmentApiKeysHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; + try { - const { items, total, page, perPage } = await APIKeyService.listEnrollmentApiKeys(soClient, { - page: request.query.page, - perPage: request.query.perPage, - kuery: request.query.kuery, - }); + const { items, total, page, perPage } = await APIKeyService.listEnrollmentApiKeys( + soClient, + esClient, + { + page: request.query.page, + perPage: request.query.perPage, + kuery: request.query.kuery, + } + ); const body: GetEnrollmentAPIKeysResponse = { list: items, total, page, perPage }; return response.ok({ body }); @@ -45,8 +51,9 @@ export const postEnrollmentApiKeyHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; try { - const apiKey = await APIKeyService.generateEnrollmentAPIKey(soClient, { + const apiKey = await APIKeyService.generateEnrollmentAPIKey(soClient, esClient, { name: request.body.name, expiration: request.body.expiration, agentPolicyId: request.body.policy_id, @@ -64,8 +71,9 @@ export const deleteEnrollmentApiKeyHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; try { - await APIKeyService.deleteEnrollmentApiKey(soClient, request.params.keyId); + await APIKeyService.deleteEnrollmentApiKey(soClient, esClient, request.params.keyId); const body: DeleteEnrollmentAPIKeyResponse = { action: 'deleted' }; @@ -84,8 +92,13 @@ export const getOneEnrollmentApiKeyHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; try { - const apiKey = await APIKeyService.getEnrollmentAPIKey(soClient, request.params.keyId); + const apiKey = await APIKeyService.getEnrollmentAPIKey( + soClient, + esClient, + request.params.keyId + ); const body: GetOneEnrollmentAPIKeyResponse = { item: apiKey }; return response.ok({ body }); diff --git a/x-pack/plugins/fleet/server/routes/setup/handlers.ts b/x-pack/plugins/fleet/server/routes/setup/handlers.ts index f87cf8026c560..b5a68ed9bb9a0 100644 --- a/x-pack/plugins/fleet/server/routes/setup/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/setup/handlers.ts @@ -62,7 +62,7 @@ export const createFleetSetupHandler: RequestHandler< const soClient = context.core.savedObjects.client; const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; await setupIngestManager(soClient, callCluster); - await setupFleet(soClient, callCluster, { + await setupFleet(soClient, context.core.elasticsearch.client.asCurrentUser, callCluster, { forceRecreate: request.body?.forceRecreate ?? false, }); diff --git a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts index b9d0cf883d35c..747cbae3f71ce 100644 --- a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts +++ b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key.ts @@ -4,18 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import uuid from 'uuid'; -import Boom from '@hapi/boom'; -import { SavedObjectsClientContract, SavedObject } from 'src/core/server'; -import { EnrollmentAPIKey, EnrollmentAPIKeySOAttributes } from '../../types'; -import { ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE } from '../../constants'; -import { createAPIKey, invalidateAPIKey } from './security'; -import { agentPolicyService } from '../agent_policy'; +import { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; +import { EnrollmentAPIKey } from '../../types'; import { appContextService } from '../app_context'; -import { normalizeKuery } from '../saved_object'; +import * as enrollmentApiKeyServiceSO from './enrollment_api_key_so'; +import * as enrollmentApiKeyServiceFleetServer from './enrollment_api_key_fleet_server'; export async function listEnrollmentApiKeys( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, options: { page?: number; perPage?: number; @@ -23,39 +20,23 @@ export async function listEnrollmentApiKeys( showInactive?: boolean; } ): Promise<{ items: EnrollmentAPIKey[]; total: any; page: any; perPage: any }> { - const { page = 1, perPage = 20, kuery } = options; - - // eslint-disable-next-line @typescript-eslint/naming-convention - const { saved_objects, total } = await soClient.find({ - type: ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, - page, - perPage, - sortField: 'created_at', - sortOrder: 'desc', - filter: - kuery && kuery !== '' - ? normalizeKuery(ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, kuery) - : undefined, - }); - - const items = saved_objects.map(savedObjectToEnrollmentApiKey); - - return { - items, - total, - page, - perPage, - }; + if (appContextService.getConfig()?.agents?.fleetServerEnabled === true) { + return enrollmentApiKeyServiceFleetServer.listEnrollmentApiKeys(esClient, options); + } else { + return enrollmentApiKeyServiceSO.listEnrollmentApiKeys(soClient, options); + } } -export async function getEnrollmentAPIKey(soClient: SavedObjectsClientContract, id: string) { - const so = await appContextService - .getEncryptedSavedObjects() - .getDecryptedAsInternalUser( - ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, - id - ); - return savedObjectToEnrollmentApiKey(so); +export async function getEnrollmentAPIKey( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + id: string +) { + if (appContextService.getConfig()?.agents?.fleetServerEnabled === true) { + return enrollmentApiKeyServiceFleetServer.getEnrollmentAPIKey(esClient, id); + } else { + return enrollmentApiKeyServiceSO.getEnrollmentAPIKey(soClient, id); + } } /** @@ -63,112 +44,37 @@ export async function getEnrollmentAPIKey(soClient: SavedObjectsClientContract, * @param soClient * @param id */ -export async function deleteEnrollmentApiKey(soClient: SavedObjectsClientContract, id: string) { - const enrollmentApiKey = await getEnrollmentAPIKey(soClient, id); - - await invalidateAPIKey(soClient, enrollmentApiKey.api_key_id); - - await soClient.update(ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, id, { - active: false, - }); +export async function deleteEnrollmentApiKey( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + id: string +) { + if (appContextService.getConfig()?.agents?.fleetServerEnabled === true) { + return enrollmentApiKeyServiceFleetServer.deleteEnrollmentApiKey(soClient, esClient, id); + } else { + return enrollmentApiKeyServiceSO.deleteEnrollmentApiKey(soClient, id); + } } export async function deleteEnrollmentApiKeyForAgentPolicyId( soClient: SavedObjectsClientContract, agentPolicyId: string ) { - let hasMore = true; - let page = 1; - while (hasMore) { - const { items } = await listEnrollmentApiKeys(soClient, { - page: page++, - perPage: 100, - kuery: `${ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE}.policy_id:${agentPolicyId}`, - }); - - if (items.length === 0) { - hasMore = false; - } - - for (const apiKey of items) { - await deleteEnrollmentApiKey(soClient, apiKey.id); - } - } + return enrollmentApiKeyServiceSO.deleteEnrollmentApiKeyForAgentPolicyId(soClient, agentPolicyId); } export async function generateEnrollmentAPIKey( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, data: { name?: string; expiration?: string; agentPolicyId?: string; } ) { - const id = uuid.v4(); - const { name: providedKeyName } = data; - if (data.agentPolicyId) { - await validateAgentPolicyId(soClient, data.agentPolicyId); - } - const agentPolicyId = - data.agentPolicyId ?? (await agentPolicyService.getDefaultAgentPolicyId(soClient)); - const name = providedKeyName ? `${providedKeyName} (${id})` : id; - const key = await createAPIKey(soClient, name, { - // Useless role to avoid to have the privilege of the user that created the key - 'fleet-apikey-enroll': { - cluster: [], - applications: [ - { - application: '.fleet', - privileges: ['no-privileges'], - resources: ['*'], - }, - ], - }, - }); - - if (!key) { - throw new Error('Unable to create an enrollment api key'); + if (appContextService.getConfig()?.agents?.fleetServerEnabled === true) { + return enrollmentApiKeyServiceFleetServer.generateEnrollmentAPIKey(soClient, esClient, data); + } else { + return enrollmentApiKeyServiceSO.generateEnrollmentAPIKey(soClient, data); } - - const apiKey = Buffer.from(`${key.id}:${key.api_key}`).toString('base64'); - - const so = await soClient.create( - ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, - { - active: true, - api_key_id: key.id, - api_key: apiKey, - name, - policy_id: agentPolicyId, - created_at: new Date().toISOString(), - } - ); - - return getEnrollmentAPIKey(soClient, so.id); -} - -async function validateAgentPolicyId(soClient: SavedObjectsClientContract, agentPolicyId: string) { - try { - await agentPolicyService.get(soClient, agentPolicyId); - } catch (e) { - if (e.isBoom && e.output.statusCode === 404) { - throw Boom.badRequest(`Agent policy ${agentPolicyId} does not exist`); - } - throw e; - } -} - -function savedObjectToEnrollmentApiKey({ - error, - attributes, - id, -}: SavedObject): EnrollmentAPIKey { - if (error) { - throw new Error(error.message); - } - - return { - id, - ...attributes, - }; } diff --git a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_fleet_server.ts b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_fleet_server.ts new file mode 100644 index 0000000000000..c33f2e6e095dd --- /dev/null +++ b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_fleet_server.ts @@ -0,0 +1,205 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import uuid from 'uuid'; +import Boom from '@hapi/boom'; +import { ResponseError } from '@elastic/elasticsearch/lib/errors'; +import { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; +import { EnrollmentAPIKey, FleetServerEnrollmentAPIKey } from '../../types'; +import { ENROLLMENT_API_KEYS_INDEX } from '../../constants'; +import { createAPIKey, invalidateAPIKey } from './security'; +import { agentPolicyService } from '../agent_policy'; + +// TODO Move these types to another file +interface SearchResponse { + took: number; + timed_out: boolean; + _scroll_id?: string; + hits: { + total: { + value: number; + relation: string; + }; + max_score: number; + hits: Array<{ + _index: string; + _type: string; + _id: string; + _score: number; + _source: T; + _version?: number; + fields?: any; + highlight?: any; + inner_hits?: any; + matched_queries?: string[]; + sort?: string[]; + }>; + }; +} + +type SearchHit = SearchResponse['hits']['hits'][0]; + +export async function listEnrollmentApiKeys( + esClient: ElasticsearchClient, + options: { + page?: number; + perPage?: number; + kuery?: string; + showInactive?: boolean; + } +): Promise<{ items: EnrollmentAPIKey[]; total: any; page: any; perPage: any }> { + const { page = 1, perPage = 20, kuery } = options; + + const res = await esClient.search>({ + index: ENROLLMENT_API_KEYS_INDEX, + from: (page - 1) * perPage, + size: perPage, + sort: 'created_at:desc', + track_total_hits: true, + // TODO support kuery + }); + + const items = res.body.hits.hits.map(esDocToEnrollmentApiKey); + + return { + items, + total: res.body.hits.total.value, + page, + perPage, + }; +} + +export async function getEnrollmentAPIKey( + esClient: ElasticsearchClient, + id: string +): Promise { + try { + const res = await esClient.get>({ + index: ENROLLMENT_API_KEYS_INDEX, + id, + }); + + return esDocToEnrollmentApiKey(res.body); + } catch (e) { + if (e instanceof ResponseError && e.statusCode === 404) { + throw Boom.notFound(`Enrollment api key ${id} not found`); + } + + throw e; + } +} + +/** + * Invalidate an api key and mark it as inactive + * @param soClient + * @param id + */ +export async function deleteEnrollmentApiKey( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + id: string +) { + const enrollmentApiKey = await getEnrollmentAPIKey(esClient, id); + + await invalidateAPIKey(soClient, enrollmentApiKey.api_key_id); + + await esClient.update({ + index: ENROLLMENT_API_KEYS_INDEX, + id, + body: { + doc: { + active: false, + }, + }, + refresh: 'wait_for', + }); +} + +export async function deleteEnrollmentApiKeyForAgentPolicyId( + soClient: SavedObjectsClientContract, + agentPolicyId: string +) { + throw new Error('NOT IMPLEMENTED'); +} + +export async function generateEnrollmentAPIKey( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + data: { + name?: string; + expiration?: string; + agentPolicyId?: string; + } +): Promise { + const id = uuid.v4(); + const { name: providedKeyName } = data; + if (data.agentPolicyId) { + await validateAgentPolicyId(soClient, data.agentPolicyId); + } + const agentPolicyId = + data.agentPolicyId ?? (await agentPolicyService.getDefaultAgentPolicyId(soClient)); + const name = providedKeyName ? `${providedKeyName} (${id})` : id; + const key = await createAPIKey(soClient, name, { + // Useless role to avoid to have the privilege of the user that created the key + 'fleet-apikey-enroll': { + cluster: [], + applications: [ + { + application: '.fleet', + privileges: ['no-privileges'], + resources: ['*'], + }, + ], + }, + }); + + if (!key) { + throw new Error('Unable to create an enrollment api key'); + } + + const apiKey = Buffer.from(`${key.id}:${key.api_key}`).toString('base64'); + + const body = { + active: true, + api_key_id: key.id, + api_key: apiKey, + name, + policy_id: agentPolicyId, + created_at: new Date().toISOString(), + }; + + const res = await esClient.create({ + index: ENROLLMENT_API_KEYS_INDEX, + body, + id, + refresh: 'wait_for', + }); + + return { + id: res.body._id, + ...body, + }; +} + +async function validateAgentPolicyId(soClient: SavedObjectsClientContract, agentPolicyId: string) { + try { + await agentPolicyService.get(soClient, agentPolicyId); + } catch (e) { + if (e.isBoom && e.output.statusCode === 404) { + throw Boom.badRequest(`Agent policy ${agentPolicyId} does not exist`); + } + throw e; + } +} + +function esDocToEnrollmentApiKey(doc: SearchHit): EnrollmentAPIKey { + return { + id: doc._id, + ...doc._source, + created_at: doc._source.created_at as string, + active: doc._source.active || false, + }; +} diff --git a/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_so.ts b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_so.ts new file mode 100644 index 0000000000000..b9d0cf883d35c --- /dev/null +++ b/x-pack/plugins/fleet/server/services/api_keys/enrollment_api_key_so.ts @@ -0,0 +1,174 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import uuid from 'uuid'; +import Boom from '@hapi/boom'; +import { SavedObjectsClientContract, SavedObject } from 'src/core/server'; +import { EnrollmentAPIKey, EnrollmentAPIKeySOAttributes } from '../../types'; +import { ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE } from '../../constants'; +import { createAPIKey, invalidateAPIKey } from './security'; +import { agentPolicyService } from '../agent_policy'; +import { appContextService } from '../app_context'; +import { normalizeKuery } from '../saved_object'; + +export async function listEnrollmentApiKeys( + soClient: SavedObjectsClientContract, + options: { + page?: number; + perPage?: number; + kuery?: string; + showInactive?: boolean; + } +): Promise<{ items: EnrollmentAPIKey[]; total: any; page: any; perPage: any }> { + const { page = 1, perPage = 20, kuery } = options; + + // eslint-disable-next-line @typescript-eslint/naming-convention + const { saved_objects, total } = await soClient.find({ + type: ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, + page, + perPage, + sortField: 'created_at', + sortOrder: 'desc', + filter: + kuery && kuery !== '' + ? normalizeKuery(ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, kuery) + : undefined, + }); + + const items = saved_objects.map(savedObjectToEnrollmentApiKey); + + return { + items, + total, + page, + perPage, + }; +} + +export async function getEnrollmentAPIKey(soClient: SavedObjectsClientContract, id: string) { + const so = await appContextService + .getEncryptedSavedObjects() + .getDecryptedAsInternalUser( + ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, + id + ); + return savedObjectToEnrollmentApiKey(so); +} + +/** + * Invalidate an api key and mark it as inactive + * @param soClient + * @param id + */ +export async function deleteEnrollmentApiKey(soClient: SavedObjectsClientContract, id: string) { + const enrollmentApiKey = await getEnrollmentAPIKey(soClient, id); + + await invalidateAPIKey(soClient, enrollmentApiKey.api_key_id); + + await soClient.update(ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, id, { + active: false, + }); +} + +export async function deleteEnrollmentApiKeyForAgentPolicyId( + soClient: SavedObjectsClientContract, + agentPolicyId: string +) { + let hasMore = true; + let page = 1; + while (hasMore) { + const { items } = await listEnrollmentApiKeys(soClient, { + page: page++, + perPage: 100, + kuery: `${ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE}.policy_id:${agentPolicyId}`, + }); + + if (items.length === 0) { + hasMore = false; + } + + for (const apiKey of items) { + await deleteEnrollmentApiKey(soClient, apiKey.id); + } + } +} + +export async function generateEnrollmentAPIKey( + soClient: SavedObjectsClientContract, + data: { + name?: string; + expiration?: string; + agentPolicyId?: string; + } +) { + const id = uuid.v4(); + const { name: providedKeyName } = data; + if (data.agentPolicyId) { + await validateAgentPolicyId(soClient, data.agentPolicyId); + } + const agentPolicyId = + data.agentPolicyId ?? (await agentPolicyService.getDefaultAgentPolicyId(soClient)); + const name = providedKeyName ? `${providedKeyName} (${id})` : id; + const key = await createAPIKey(soClient, name, { + // Useless role to avoid to have the privilege of the user that created the key + 'fleet-apikey-enroll': { + cluster: [], + applications: [ + { + application: '.fleet', + privileges: ['no-privileges'], + resources: ['*'], + }, + ], + }, + }); + + if (!key) { + throw new Error('Unable to create an enrollment api key'); + } + + const apiKey = Buffer.from(`${key.id}:${key.api_key}`).toString('base64'); + + const so = await soClient.create( + ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, + { + active: true, + api_key_id: key.id, + api_key: apiKey, + name, + policy_id: agentPolicyId, + created_at: new Date().toISOString(), + } + ); + + return getEnrollmentAPIKey(soClient, so.id); +} + +async function validateAgentPolicyId(soClient: SavedObjectsClientContract, agentPolicyId: string) { + try { + await agentPolicyService.get(soClient, agentPolicyId); + } catch (e) { + if (e.isBoom && e.output.statusCode === 404) { + throw Boom.badRequest(`Agent policy ${agentPolicyId} does not exist`); + } + throw e; + } +} + +function savedObjectToEnrollmentApiKey({ + error, + attributes, + id, +}: SavedObject): EnrollmentAPIKey { + if (error) { + throw new Error(error.message); + } + + return { + id, + ...attributes, + }; +} diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts index a3df7bc3dcdc4..1cc26b8606907 100644 --- a/x-pack/plugins/fleet/server/services/setup.ts +++ b/x-pack/plugins/fleet/server/services/setup.ts @@ -5,7 +5,7 @@ */ import uuid from 'uuid'; -import { SavedObjectsClientContract } from 'src/core/server'; +import { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; import { CallESAsCurrentUser } from '../types'; import { agentPolicyService } from './agent_policy'; import { outputService } from './output'; @@ -127,6 +127,7 @@ async function createSetupSideEffects( export async function setupFleet( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, callCluster: CallESAsCurrentUser, options?: { forceRecreate?: boolean } ) { @@ -182,7 +183,7 @@ export async function setupFleet( await Promise.all( agentPolicies.map((agentPolicy) => { - return generateEnrollmentAPIKey(soClient, { + return generateEnrollmentAPIKey(soClient, esClient, { name: `Default`, agentPolicyId: agentPolicy.id, }); diff --git a/x-pack/plugins/fleet/server/types/index.tsx b/x-pack/plugins/fleet/server/types/index.tsx index 7e6e6d5e408b4..fbac999b2770c 100644 --- a/x-pack/plugins/fleet/server/types/index.tsx +++ b/x-pack/plugins/fleet/server/types/index.tsx @@ -72,6 +72,8 @@ export { SettingsSOAttributes, InstallType, InstallSource, + // Fleet Server types + FleetServerEnrollmentAPIKey, // Agent Request types PostAgentEnrollRequest, PostAgentCheckinRequest,