From 3b728b73cf5720b48e9759c7bb8b845e18ba5b57 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Wed, 20 Jan 2021 19:29:04 -0500 Subject: [PATCH] [Fleet] Use fleet server indices for enrollment keys and to list agents with a feature flag (#86179) --- .../plugins/fleet/common/constants/agent.ts | 2 + .../fleet/common/constants/agent_policy.ts | 2 +- .../common/constants/enrollment_api_key.ts | 2 + .../fleet/common/services/agent_status.ts | 4 +- x-pack/plugins/fleet/common/types/index.ts | 1 + .../fleet/common/types/models/agent_policy.ts | 34 +++ .../common/types/models/enrollment_api_key.ts | 28 +++ .../fleet/mock/plugin_configuration.ts | 1 + .../server/collectors/agent_collectors.ts | 12 +- .../fleet/server/collectors/helpers.ts | 9 +- .../fleet/server/collectors/register.ts | 6 +- .../plugins/fleet/server/constants/index.ts | 3 + x-pack/plugins/fleet/server/index.ts | 1 + x-pack/plugins/fleet/server/mocks.ts | 7 +- x-pack/plugins/fleet/server/plugin.ts | 11 + .../server/routes/agent/acks_handlers.test.ts | 10 +- .../server/routes/agent/acks_handlers.ts | 8 +- .../routes/agent/actions_handlers.test.ts | 14 +- .../server/routes/agent/actions_handlers.ts | 3 +- .../fleet/server/routes/agent/handlers.ts | 34 ++- .../fleet/server/routes/agent/index.ts | 6 + .../server/routes/agent/unenroll_handler.ts | 8 +- .../server/routes/agent/upgrade_handler.ts | 5 +- .../server/routes/agent_policy/handlers.ts | 14 +- .../routes/enrollment_api_key/handler.ts | 29 ++- .../routes/package_policy/handlers.test.ts | 4 +- .../server/routes/package_policy/handlers.ts | 17 +- .../fleet/server/routes/settings/index.ts | 4 +- .../fleet/server/routes/setup/handlers.ts | 8 +- .../server/services/agent_policy.test.ts | 10 +- .../fleet/server/services/agent_policy.ts | 117 ++++++++-- .../server/services/agent_policy_update.ts | 7 +- .../fleet/server/services/agents/acks.test.ts | 16 +- .../fleet/server/services/agents/acks.ts | 7 +- .../fleet/server/services/agents/actions.ts | 8 +- .../server/services/agents/checkin/index.ts | 9 +- .../server/services/agents/checkin/state.ts | 5 +- .../agents/checkin/state_new_actions.ts | 5 +- .../fleet/server/services/agents/crud.ts | 165 ++++---------- .../services/agents/crud_fleet_server.ts | 197 +++++++++++++++++ .../fleet/server/services/agents/crud_so.ts | 195 +++++++++++++++++ .../fleet/server/services/agents/helpers.ts | 21 ++ .../fleet/server/services/agents/reassign.ts | 6 +- .../server/services/agents/status.test.ts | 14 +- .../fleet/server/services/agents/status.ts | 8 +- .../fleet/server/services/agents/unenroll.ts | 16 +- .../fleet/server/services/agents/update.ts | 5 +- .../fleet/server/services/agents/upgrade.ts | 5 +- .../services/api_keys/enrollment_api_key.ts | 166 +++----------- .../enrollment_api_key_fleet_server.ts | 205 ++++++++++++++++++ .../api_keys/enrollment_api_key_so.ts | 174 +++++++++++++++ .../fleet/server/services/app_context.ts | 20 +- .../server/services/fleet_server_migration.ts | 75 +++++++ x-pack/plugins/fleet/server/services/index.ts | 8 +- .../server/services/package_policy.test.ts | 4 +- .../fleet/server/services/package_policy.ts | 29 ++- .../fleet/server/services/setup.test.ts | 6 +- x-pack/plugins/fleet/server/services/setup.ts | 15 +- x-pack/plugins/fleet/server/types/index.tsx | 2 + .../endpoint/lib/policy/license_watch.test.ts | 15 +- .../endpoint/lib/policy/license_watch.ts | 14 +- .../endpoint/routes/metadata/handlers.ts | 7 +- .../metadata/support/agent_status.test.ts | 37 +++- .../routes/metadata/support/agent_status.ts | 5 +- .../routes/metadata/support/unenroll.test.ts | 15 +- .../routes/metadata/support/unenroll.ts | 9 +- .../server/endpoint/routes/policy/handlers.ts | 1 + .../server/endpoint/routes/policy/service.ts | 13 +- .../manifest_manager/manifest_manager.test.ts | 2 +- .../manifest_manager/manifest_manager.ts | 8 +- .../security_solution/server/plugin.ts | 1 + 71 files changed, 1528 insertions(+), 406 deletions(-) create mode 100644 x-pack/plugins/fleet/server/services/agents/crud_fleet_server.ts create mode 100644 x-pack/plugins/fleet/server/services/agents/crud_so.ts create mode 100644 x-pack/plugins/fleet/server/services/agents/helpers.ts 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 create mode 100644 x-pack/plugins/fleet/server/services/fleet_server_migration.ts diff --git a/x-pack/plugins/fleet/common/constants/agent.ts b/x-pack/plugins/fleet/common/constants/agent.ts index 30b8a6b74060..8bfb32b5ed2b 100644 --- a/x-pack/plugins/fleet/common/constants/agent.ts +++ b/x-pack/plugins/fleet/common/constants/agent.ts @@ -22,3 +22,5 @@ export const AGENT_UPDATE_ACTIONS_INTERVAL_MS = 5000; export const AGENT_POLICY_ROLLOUT_RATE_LIMIT_INTERVAL_MS = 1000; export const AGENT_POLICY_ROLLOUT_RATE_LIMIT_REQUEST_PER_INTERVAL = 5; + +export const AGENTS_INDEX = '.fleet-agents'; diff --git a/x-pack/plugins/fleet/common/constants/agent_policy.ts b/x-pack/plugins/fleet/common/constants/agent_policy.ts index 5445fbcacf2e..2dd21fb41b66 100644 --- a/x-pack/plugins/fleet/common/constants/agent_policy.ts +++ b/x-pack/plugins/fleet/common/constants/agent_policy.ts @@ -6,7 +6,7 @@ import { defaultPackages } from './epm'; import { AgentPolicy } from '../types'; export const AGENT_POLICY_SAVED_OBJECT_TYPE = 'ingest-agent-policies'; - +export const AGENT_POLICY_INDEX = '.fleet-policies'; export const agentPolicyStatuses = { Active: 'active', Inactive: 'inactive', 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 fd28b6632b15..ce774f221246 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/services/agent_status.ts b/x-pack/plugins/fleet/common/services/agent_status.ts index 4cf35398bab2..c99cfd11b763 100644 --- a/x-pack/plugins/fleet/common/services/agent_status.ts +++ b/x-pack/plugins/fleet/common/services/agent_status.ts @@ -41,7 +41,7 @@ export function getAgentStatus(agent: Agent, now: number = Date.now()): AgentSta } export function buildKueryForEnrollingAgents() { - return `not ${AGENT_SAVED_OBJECT_TYPE}.last_checkin:*`; + return `not (${AGENT_SAVED_OBJECT_TYPE}.last_checkin:*)`; } export function buildKueryForUnenrollingAgents() { @@ -53,7 +53,7 @@ export function buildKueryForOnlineAgents() { } export function buildKueryForErrorAgents() { - return `( ${AGENT_SAVED_OBJECT_TYPE}.last_checkin_status:error or ${AGENT_SAVED_OBJECT_TYPE}.last_checkin_status:degraded )`; + return `${AGENT_SAVED_OBJECT_TYPE}.last_checkin_status:error or ${AGENT_SAVED_OBJECT_TYPE}.last_checkin_status:degraded`; } export function buildKueryForOfflineAgents() { diff --git a/x-pack/plugins/fleet/common/types/index.ts b/x-pack/plugins/fleet/common/types/index.ts index e0827ef7cf40..b023052b1328 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/agent_policy.ts b/x-pack/plugins/fleet/common/types/models/agent_policy.ts index 75bb2998f2d9..2e29fe148b35 100644 --- a/x-pack/plugins/fleet/common/types/models/agent_policy.ts +++ b/x-pack/plugins/fleet/common/types/models/agent_policy.ts @@ -80,3 +80,37 @@ export interface FullAgentPolicyKibanaConfig { protocol: string; path?: string; } + +// Generated from Fleet Server schema.json + +/** + * A policy that an Elastic Agent is attached to + */ +export interface FleetServerPolicy { + /** + * Date/time the policy revision was created + */ + '@timestamp'?: string; + /** + * The ID of the policy + */ + policy_id: string; + /** + * The revision index of the policy + */ + revision_idx: number; + /** + * The coordinator index of the policy + */ + coordinator_idx: number; + /** + * The opaque payload. + */ + data: { + [k: string]: unknown; + }; + /** + * True when this policy is the default policy to start Fleet Server + */ + default_fleet_server: boolean; +} 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 f39076ce1027..81dc6889f994 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/public/applications/fleet/mock/plugin_configuration.ts b/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_configuration.ts index 735c1d11a983..62896289af51 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_configuration.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/mock/plugin_configuration.ts @@ -13,6 +13,7 @@ export const createConfigurationMock = (): FleetConfigType => { registryProxyUrl: '', agents: { enabled: true, + fleetServerEnabled: false, tlsCheckDisabled: true, pollingRequestTimeout: 1000, maxConcurrentConnections: 100, diff --git a/x-pack/plugins/fleet/server/collectors/agent_collectors.ts b/x-pack/plugins/fleet/server/collectors/agent_collectors.ts index fe5e5fa735b2..8925f3386dfb 100644 --- a/x-pack/plugins/fleet/server/collectors/agent_collectors.ts +++ b/x-pack/plugins/fleet/server/collectors/agent_collectors.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClient } from 'kibana/server'; +import { ElasticsearchClient, SavedObjectsClient } from 'kibana/server'; import * as AgentService from '../services/agents'; export interface AgentUsage { total: number; @@ -13,9 +13,12 @@ export interface AgentUsage { offline: number; } -export const getAgentUsage = async (soClient?: SavedObjectsClient): Promise => { +export const getAgentUsage = async ( + soClient?: SavedObjectsClient, + esClient?: ElasticsearchClient +): Promise => { // TODO: unsure if this case is possible at all. - if (!soClient) { + if (!soClient || !esClient) { return { total: 0, online: 0, @@ -24,7 +27,8 @@ export const getAgentUsage = async (soClient?: SavedObjectsClient): Promise { return core.getStartServices().then(async ([coreStart]) => { const savedObjectsRepo = coreStart.savedObjects.createInternalRepository(); - return new SavedObjectsClient(savedObjectsRepo); + const esClient = coreStart.elasticsearch.client.asInternalUser; + return [new SavedObjectsClient(savedObjectsRepo), esClient]; }); } diff --git a/x-pack/plugins/fleet/server/collectors/register.ts b/x-pack/plugins/fleet/server/collectors/register.ts index 35517e6a7a70..7ec04ca6fee4 100644 --- a/x-pack/plugins/fleet/server/collectors/register.ts +++ b/x-pack/plugins/fleet/server/collectors/register.ts @@ -8,7 +8,7 @@ import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { CoreSetup } from 'kibana/server'; import { getIsAgentsEnabled } from './config_collectors'; import { AgentUsage, getAgentUsage } from './agent_collectors'; -import { getInternalSavedObjectsClient } from './helpers'; +import { getInternalClients } from './helpers'; import { PackageUsage, getPackageUsage } from './package_collectors'; import { FleetConfigType } from '..'; @@ -34,10 +34,10 @@ export function registerFleetUsageCollector( type: 'fleet', isReady: () => true, fetch: async () => { - const soClient = await getInternalSavedObjectsClient(core); + const [soClient, esClient] = await getInternalClients(core); return { agents_enabled: getIsAgentsEnabled(config), - agents: await getAgentUsage(soClient), + agents: await getAgentUsage(soClient, esClient), packages: await getPackageUsage(soClient), }; }, diff --git a/x-pack/plugins/fleet/server/constants/index.ts b/x-pack/plugins/fleet/server/constants/index.ts index dbf2fbc362a4..37f8ab041e5a 100644 --- a/x-pack/plugins/fleet/server/constants/index.ts +++ b/x-pack/plugins/fleet/server/constants/index.ts @@ -47,4 +47,7 @@ export { // Defaults DEFAULT_AGENT_POLICY, DEFAULT_OUTPUT, + // Fleet Server index + ENROLLMENT_API_KEYS_INDEX, + AGENTS_INDEX, } from '../../common'; diff --git a/x-pack/plugins/fleet/server/index.ts b/x-pack/plugins/fleet/server/index.ts index 1fe7013944fd..672911ccf6fe 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/mocks.ts b/x-pack/plugins/fleet/server/mocks.ts index 4a897d80acd6..bf659294f514 100644 --- a/x-pack/plugins/fleet/server/mocks.ts +++ b/x-pack/plugins/fleet/server/mocks.ts @@ -4,7 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { loggingSystemMock, savedObjectsServiceMock } from 'src/core/server/mocks'; +import { + elasticsearchServiceMock, + loggingSystemMock, + savedObjectsServiceMock, +} from 'src/core/server/mocks'; import { FleetAppContext } from './plugin'; import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks'; import { securityMock } from '../../security/server/mocks'; @@ -13,6 +17,7 @@ import { AgentPolicyServiceInterface, AgentService } from './services'; export const createAppContextStartContractMock = (): FleetAppContext => { return { + elasticsearch: elasticsearchServiceMock.createStart(), encryptedSavedObjectsStart: encryptedSavedObjectsMock.createStart(), savedObjects: savedObjectsServiceMock.createStartContract(), security: securityMock.createStart(), diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 8ce17a00acf3..253b614dc228 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -8,6 +8,7 @@ import { first } from 'rxjs/operators'; import { CoreSetup, CoreStart, + ElasticsearchServiceStart, Logger, Plugin, PluginInitializerContext, @@ -80,6 +81,7 @@ import { agentCheckinState } from './services/agents/checkin/state'; import { registerFleetUsageCollector } from './collectors/register'; import { getInstallation } from './services/epm/packages'; import { makeRouterEnforcingSuperuser } from './routes/security'; +import { runFleetServerMigration } from './services/fleet_server_migration'; export interface FleetSetupDeps { licensing: LicensingPluginSetup; @@ -96,6 +98,7 @@ export interface FleetStartDeps { } export interface FleetAppContext { + elasticsearch: ElasticsearchServiceStart; encryptedSavedObjectsStart?: EncryptedSavedObjectsPluginStart; encryptedSavedObjectsSetup?: EncryptedSavedObjectsPluginSetup; security?: SecurityPluginStart; @@ -276,6 +279,7 @@ export class FleetPlugin public async start(core: CoreStart, plugins: FleetStartDeps): Promise { await appContextService.start({ + elasticsearch: core.elasticsearch, encryptedSavedObjectsStart: plugins.encryptedSavedObjects, encryptedSavedObjectsSetup: this.encryptedSavedObjectsSetup, security: plugins.security, @@ -291,6 +295,13 @@ export class FleetPlugin licenseService.start(this.licensing$); agentCheckinState.start(); + const fleetServerEnabled = appContextService.getConfig()?.agents?.fleetServerEnabled; + if (fleetServerEnabled) { + // We need licence to be initialized before using the SO service. + await this.licensing$.pipe(first()).toPromise(); + await runFleetServerMigration(); + } + return { esIndexPatternService: new ESIndexPatternSavedObjectService(), packageService: { diff --git a/x-pack/plugins/fleet/server/routes/agent/acks_handlers.test.ts b/x-pack/plugins/fleet/server/routes/agent/acks_handlers.test.ts index 3d7f5c4a17ad..d775979527af 100644 --- a/x-pack/plugins/fleet/server/routes/agent/acks_handlers.test.ts +++ b/x-pack/plugins/fleet/server/routes/agent/acks_handlers.test.ts @@ -6,11 +6,16 @@ import { postAgentAcksHandlerBuilder } from './acks_handlers'; import { + ElasticsearchClient, KibanaResponseFactory, RequestHandlerContext, SavedObjectsClientContract, } from 'kibana/server'; -import { httpServerMock, savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; +import { + elasticsearchServiceMock, + httpServerMock, + savedObjectsClientMock, +} from '../../../../../../src/core/server/mocks'; import { PostAgentAcksResponse } from '../../../common/types/rest_spec'; import { AckEventSchema } from '../../types/models'; import { AcksService } from '../../services/agents'; @@ -45,9 +50,11 @@ describe('test acks schema', () => { describe('test acks handlers', () => { let mockResponse: jest.Mocked; let mockSavedObjectsClient: jest.Mocked; + let mockElasticsearchClient: jest.Mocked; beforeEach(() => { mockSavedObjectsClient = savedObjectsClientMock.create(); + mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; mockResponse = httpServerMock.createResponseFactory(); }); @@ -81,6 +88,7 @@ describe('test acks handlers', () => { id: 'agent', }), getSavedObjectsClientContract: jest.fn().mockReturnValueOnce(mockSavedObjectsClient), + getElasticsearchClientContract: jest.fn().mockReturnValueOnce(mockElasticsearchClient), saveAgentEvents: jest.fn(), } as jest.Mocked; diff --git a/x-pack/plugins/fleet/server/routes/agent/acks_handlers.ts b/x-pack/plugins/fleet/server/routes/agent/acks_handlers.ts index fb320b01dea9..28cd7e57d653 100644 --- a/x-pack/plugins/fleet/server/routes/agent/acks_handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/acks_handlers.ts @@ -18,6 +18,7 @@ export const postAgentAcksHandlerBuilder = function ( return async (context, request, response) => { try { const soClient = ackService.getSavedObjectsClientContract(request); + const esClient = ackService.getElasticsearchClientContract(); const agent = await ackService.authenticateAgentWithAccessToken(soClient, request); const agentEvents = request.body.events as AgentEvent[]; @@ -33,7 +34,12 @@ export const postAgentAcksHandlerBuilder = function ( }); } - const agentActions = await ackService.acknowledgeAgentActions(soClient, agent, agentEvents); + const agentActions = await ackService.acknowledgeAgentActions( + soClient, + esClient, + agent, + agentEvents + ); if (agentActions.length > 0) { await ackService.saveAgentEvents(soClient, agentEvents); diff --git a/x-pack/plugins/fleet/server/routes/agent/actions_handlers.test.ts b/x-pack/plugins/fleet/server/routes/agent/actions_handlers.test.ts index 2f0884664298..2674e8c5cedd 100644 --- a/x-pack/plugins/fleet/server/routes/agent/actions_handlers.test.ts +++ b/x-pack/plugins/fleet/server/routes/agent/actions_handlers.test.ts @@ -6,11 +6,16 @@ import { NewAgentActionSchema } from '../../types/models'; import { + ElasticsearchClient, KibanaResponseFactory, RequestHandlerContext, SavedObjectsClientContract, } from 'kibana/server'; -import { savedObjectsClientMock, httpServerMock } from 'src/core/server/mocks'; +import { + elasticsearchServiceMock, + savedObjectsClientMock, + httpServerMock, +} from 'src/core/server/mocks'; import { ActionsService } from '../../services/agents'; import { AgentAction } from '../../../common/types/models'; import { postNewAgentActionHandlerBuilder } from './actions_handlers'; @@ -41,9 +46,11 @@ describe('test actions handlers schema', () => { describe('test actions handlers', () => { let mockResponse: jest.Mocked; let mockSavedObjectsClient: jest.Mocked; + let mockElasticsearchClient: jest.Mocked; beforeEach(() => { mockSavedObjectsClient = savedObjectsClientMock.create(); + mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; mockResponse = httpServerMock.createResponseFactory(); }); @@ -84,6 +91,11 @@ describe('test actions handlers', () => { savedObjects: { client: mockSavedObjectsClient, }, + elasticsearch: { + client: { + asInternalUser: mockElasticsearchClient, + }, + }, }, } as unknown) as RequestHandlerContext, mockRequest, diff --git a/x-pack/plugins/fleet/server/routes/agent/actions_handlers.ts b/x-pack/plugins/fleet/server/routes/agent/actions_handlers.ts index 64a7795cc9da..04b92296439c 100644 --- a/x-pack/plugins/fleet/server/routes/agent/actions_handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/actions_handlers.ts @@ -23,8 +23,9 @@ export const postNewAgentActionHandlerBuilder = function ( return async (context, request, response) => { try { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asInternalUser; - const agent = await actionsService.getAgent(soClient, request.params.agentId); + const agent = await actionsService.getAgent(soClient, esClient, request.params.agentId); const newAgentAction = request.body.action; diff --git a/x-pack/plugins/fleet/server/routes/agent/handlers.ts b/x-pack/plugins/fleet/server/routes/agent/handlers.ts index a867196f9762..0cd53a2313d2 100644 --- a/x-pack/plugins/fleet/server/routes/agent/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent/handlers.ts @@ -39,8 +39,10 @@ export const getAgentHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; + try { - const agent = await AgentService.getAgent(soClient, request.params.agentId); + const agent = await AgentService.getAgent(soClient, esClient, request.params.agentId); const body: GetOneAgentResponse = { item: { @@ -98,8 +100,10 @@ export const deleteAgentHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; + try { - await AgentService.deleteAgent(soClient, request.params.agentId); + await AgentService.deleteAgent(soClient, esClient, request.params.agentId); const body = { action: 'deleted', @@ -124,11 +128,13 @@ export const updateAgentHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; + try { await AgentService.updateAgent(soClient, request.params.agentId, { userProvidedMetatada: request.body.user_provided_metadata, }); - const agent = await AgentService.getAgent(soClient, request.params.agentId); + const agent = await AgentService.getAgent(soClient, esClient, request.params.agentId); const body = { item: { @@ -156,6 +162,7 @@ export const postAgentCheckinHandler: RequestHandler< > = async (context, request, response) => { try { const soClient = appContextService.getInternalUserSOClient(request); + const esClient = appContextService.getInternalUserESClient(); const agent = await AgentService.authenticateAgentWithAccessToken(soClient, request); const abortController = new AbortController(); request.events.aborted$.subscribe(() => { @@ -164,6 +171,7 @@ export const postAgentCheckinHandler: RequestHandler< const signal = abortController.signal; const { actions } = await AgentService.agentCheckin( soClient, + esClient, agent, { events: request.body.events || [], @@ -234,8 +242,10 @@ export const getAgentsHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; + try { - const { agents, total, page, perPage } = await AgentService.listAgents(soClient, { + const { agents, total, page, perPage } = await AgentService.listAgents(soClient, esClient, { page: request.query.page, perPage: request.query.perPage, showInactive: request.query.showInactive, @@ -243,7 +253,7 @@ export const getAgentsHandler: RequestHandler< kuery: request.query.kuery, }); const totalInactive = request.query.showInactive - ? await AgentService.countInactiveAgents(soClient, { + ? await AgentService.countInactiveAgents(soClient, esClient, { kuery: request.query.kuery, }) : 0; @@ -270,8 +280,14 @@ export const putAgentsReassignHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asInternalUser; try { - await AgentService.reassignAgent(soClient, request.params.agentId, request.body.policy_id); + await AgentService.reassignAgent( + soClient, + esClient, + request.params.agentId, + request.body.policy_id + ); const body: PutAgentReassignResponse = {}; return response.ok({ body }); @@ -293,16 +309,19 @@ export const postBulkAgentsReassignHandler: RequestHandler< } const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asInternalUser; try { // Reassign by array of IDs const result = Array.isArray(request.body.agents) ? await AgentService.reassignAgents( soClient, + esClient, { agentIds: request.body.agents }, request.body.policy_id ) : await AgentService.reassignAgents( soClient, + esClient, { kuery: request.body.agents }, request.body.policy_id ); @@ -326,10 +345,13 @@ export const getAgentStatusForAgentPolicyHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; + try { // TODO change path const results = await AgentService.getAgentStatusForAgentPolicy( soClient, + esClient, request.query.policyId, request.query.kuery ); diff --git a/x-pack/plugins/fleet/server/routes/agent/index.ts b/x-pack/plugins/fleet/server/routes/agent/index.ts index 54a30fbc9320..c088349a995a 100644 --- a/x-pack/plugins/fleet/server/routes/agent/index.ts +++ b/x-pack/plugins/fleet/server/routes/agent/index.ts @@ -294,6 +294,9 @@ export const registerElasticAgentRoutes = (router: IRouter, config: FleetConfigT getSavedObjectsClientContract: appContextService.getInternalUserSOClient.bind( appContextService ), + getElasticsearchClientContract: appContextService.getInternalUserESClient.bind( + appContextService + ), saveAgentEvents: AgentService.saveAgentEvents, }) ); @@ -313,6 +316,9 @@ export const registerElasticAgentRoutes = (router: IRouter, config: FleetConfigT getSavedObjectsClientContract: appContextService.getInternalUserSOClient.bind( appContextService ), + getElasticsearchClientContract: appContextService.getInternalUserESClient.bind( + appContextService + ), saveAgentEvents: AgentService.saveAgentEvents, }) ); diff --git a/x-pack/plugins/fleet/server/routes/agent/unenroll_handler.ts b/x-pack/plugins/fleet/server/routes/agent/unenroll_handler.ts index 861d7c45c6f0..41c183789f9f 100644 --- a/x-pack/plugins/fleet/server/routes/agent/unenroll_handler.ts +++ b/x-pack/plugins/fleet/server/routes/agent/unenroll_handler.ts @@ -18,9 +18,10 @@ export const postAgentUnenrollHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asInternalUser; try { if (request.body?.force === true) { - await AgentService.forceUnenrollAgent(soClient, request.params.agentId); + await AgentService.forceUnenrollAgent(soClient, esClient, request.params.agentId); } else { await AgentService.unenrollAgent(soClient, request.params.agentId); } @@ -44,14 +45,15 @@ export const postBulkAgentsUnenrollHandler: RequestHandler< }); } const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asInternalUser; const unenrollAgents = request.body?.force === true ? AgentService.forceUnenrollAgents : AgentService.unenrollAgents; try { if (Array.isArray(request.body.agents)) { - await unenrollAgents(soClient, { agentIds: request.body.agents }); + await unenrollAgents(soClient, esClient, { agentIds: request.body.agents }); } else { - await unenrollAgents(soClient, { kuery: request.body.agents }); + await unenrollAgents(soClient, esClient, { kuery: request.body.agents }); } const body: PostBulkAgentUnenrollResponse = {}; diff --git a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts index 93e6609167a2..7b068674d382 100644 --- a/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts +++ b/x-pack/plugins/fleet/server/routes/agent/upgrade_handler.ts @@ -83,6 +83,7 @@ export const postBulkAgentsUpgradeHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asInternalUser; const { version, source_uri: sourceUri, agents, force } = request.body; const kibanaVersion = appContextService.getKibanaVersion(); try { @@ -98,14 +99,14 @@ export const postBulkAgentsUpgradeHandler: RequestHandler< try { if (Array.isArray(agents)) { - await AgentService.sendUpgradeAgentsActions(soClient, { + await AgentService.sendUpgradeAgentsActions(soClient, esClient, { agentIds: agents, sourceUri, version, force, }); } else { - await AgentService.sendUpgradeAgentsActions(soClient, { + await AgentService.sendUpgradeAgentsActions(soClient, esClient, { kuery: agents, sourceUri, version, diff --git a/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts b/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts index 25aaf5f9a465..8f7fd4427f58 100644 --- a/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts @@ -39,6 +39,7 @@ export const getAgentPoliciesHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; const { full: withPackagePolicies = false, ...restOfQuery } = request.query; try { const { items, total, page, perPage } = await agentPolicyService.list(soClient, { @@ -55,7 +56,7 @@ export const getAgentPoliciesHandler: RequestHandler< await bluebird.map( items, (agentPolicy: GetAgentPoliciesResponseItem) => - listAgents(soClient, { + listAgents(soClient, esClient, { showInactive: false, perPage: 0, page: 1, @@ -100,6 +101,7 @@ export const createAgentPolicyHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; const user = (await appContextService.getSecurity()?.authc.getCurrentUser(request)) || undefined; const withSysMonitoring = request.query.sys_monitoring ?? false; @@ -109,7 +111,7 @@ export const createAgentPolicyHandler: RequestHandler< AgentPolicy, NewPackagePolicy | undefined >([ - agentPolicyService.create(soClient, request.body, { + agentPolicyService.create(soClient, esClient, request.body, { user, }), // If needed, retrieve System package information and build a new package policy for the system package @@ -126,7 +128,7 @@ export const createAgentPolicyHandler: RequestHandler< if (withSysMonitoring && newSysPackagePolicy !== undefined && agentPolicy !== undefined) { newSysPackagePolicy.policy_id = agentPolicy.id; newSysPackagePolicy.namespace = agentPolicy.namespace; - await packagePolicyService.create(soClient, callCluster, newSysPackagePolicy, { + await packagePolicyService.create(soClient, esClient, callCluster, newSysPackagePolicy, { user, bumpRevision: false, }); @@ -152,10 +154,12 @@ export const updateAgentPolicyHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; const user = await appContextService.getSecurity()?.authc.getCurrentUser(request); try { const agentPolicy = await agentPolicyService.update( soClient, + esClient, request.params.agentPolicyId, request.body, { @@ -177,10 +181,12 @@ export const copyAgentPolicyHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; const user = await appContextService.getSecurity()?.authc.getCurrentUser(request); try { const agentPolicy = await agentPolicyService.copy( soClient, + esClient, request.params.agentPolicyId, request.body, { @@ -203,9 +209,11 @@ export const deleteAgentPoliciesHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; try { const body: DeleteAgentPolicyResponse = await agentPolicyService.delete( soClient, + esClient, request.body.agentPolicyId ); return response.ok({ 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 afecd7bd7d82..4f54b4e155ea 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/package_policy/handlers.test.ts b/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts index f9fd6047baa7..90a06563efce 100644 --- a/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts +++ b/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts @@ -25,7 +25,7 @@ jest.mock('../../services/package_policy', (): { compilePackagePolicyInputs: jest.fn((packageInfo, dataInputs) => Promise.resolve(dataInputs)), buildPackagePolicyFromPackage: jest.fn(), bulkCreate: jest.fn(), - create: jest.fn((soClient, callCluster, newData) => + create: jest.fn((soClient, esClient, callCluster, newData) => Promise.resolve({ ...newData, inputs: newData.inputs.map((input) => ({ @@ -201,7 +201,7 @@ describe('When calling package policy', () => { ); await routeHandler(context, request, response); expect(response.ok).toHaveBeenCalled(); - expect(packagePolicyServiceMock.create.mock.calls[0][2]).toEqual({ + expect(packagePolicyServiceMock.create.mock.calls[0][3]).toEqual({ policy_id: 'a5ca00c0-b30c-11ea-9732-1bb05811278c', description: '', enabled: true, diff --git a/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts b/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts index be14970de3e0..bef33c1c98b6 100644 --- a/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts @@ -74,6 +74,7 @@ export const createPackagePolicyHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; const user = (await appContextService.getSecurity()?.authc.getCurrentUser(request)) || undefined; let newData = { ...request.body }; @@ -86,9 +87,15 @@ export const createPackagePolicyHandler: RequestHandler< ); // Create package policy - const packagePolicy = await packagePolicyService.create(soClient, callCluster, newData, { - user, - }); + const packagePolicy = await packagePolicyService.create( + soClient, + esClient, + callCluster, + newData, + { + user, + } + ); const body: CreatePackagePolicyResponse = { item: packagePolicy }; return response.ok({ body, @@ -110,6 +117,7 @@ export const updatePackagePolicyHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; const user = (await appContextService.getSecurity()?.authc.getCurrentUser(request)) || undefined; const packagePolicy = await packagePolicyService.get(soClient, request.params.packagePolicyId); @@ -131,6 +139,7 @@ export const updatePackagePolicyHandler: RequestHandler< const updatedPackagePolicy = await packagePolicyService.update( soClient, + esClient, request.params.packagePolicyId, { ...newData, package: pkg, inputs }, { user } @@ -149,10 +158,12 @@ export const deletePackagePolicyHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; const user = (await appContextService.getSecurity()?.authc.getCurrentUser(request)) || undefined; try { const body: DeletePackagePoliciesResponse = await packagePolicyService.delete( soClient, + esClient, request.body.packagePolicyIds, { user } ); diff --git a/x-pack/plugins/fleet/server/routes/settings/index.ts b/x-pack/plugins/fleet/server/routes/settings/index.ts index 4eeff629dc22..6f63043c12a2 100644 --- a/x-pack/plugins/fleet/server/routes/settings/index.ts +++ b/x-pack/plugins/fleet/server/routes/settings/index.ts @@ -36,10 +36,12 @@ export const putSettingsHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; const user = await appContextService.getSecurity()?.authc.getCurrentUser(request); + try { const settings = await settingsService.saveSettings(soClient, request.body); - await agentPolicyService.bumpAllAgentPolicies(soClient, { + await agentPolicyService.bumpAllAgentPolicies(soClient, esClient, { user: user || undefined, }); const body = { diff --git a/x-pack/plugins/fleet/server/routes/setup/handlers.ts b/x-pack/plugins/fleet/server/routes/setup/handlers.ts index cafccd1895d1..cf0967045f15 100644 --- a/x-pack/plugins/fleet/server/routes/setup/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/setup/handlers.ts @@ -59,9 +59,10 @@ export const createFleetSetupHandler: RequestHandler< > = async (context, request, response) => { try { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; - await setupIngestManager(soClient, callCluster); - await setupFleet(soClient, callCluster, { + await setupIngestManager(soClient, esClient, callCluster); + await setupFleet(soClient, esClient, callCluster, { forceRecreate: request.body?.forceRecreate ?? false, }); @@ -75,11 +76,12 @@ export const createFleetSetupHandler: RequestHandler< export const FleetSetupHandler: RequestHandler = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asCurrentUser; const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; try { const body: PostIngestSetupResponse = { isInitialized: true }; - await setupIngestManager(soClient, callCluster); + await setupIngestManager(soClient, esClient, callCluster); return response.ok({ body, }); diff --git a/x-pack/plugins/fleet/server/services/agent_policy.test.ts b/x-pack/plugins/fleet/server/services/agent_policy.test.ts index f9a8b63bb83a..de3647bec016 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { elasticsearchServiceMock, savedObjectsClientMock } from 'src/core/server/mocks'; import { agentPolicyService } from './agent_policy'; import { agentPolicyUpdateEventHandler } from './agent_policy_update'; import { Output } from '../types'; @@ -78,7 +78,9 @@ describe('agent policy', () => { revision: 1, monitoring_enabled: ['metrics'], }); - await agentPolicyService.bumpRevision(soClient, 'agent-policy'); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + + await agentPolicyService.bumpRevision(soClient, esClient, 'agent-policy'); expect(agentPolicyUpdateEventHandler).toHaveBeenCalledTimes(1); }); @@ -90,7 +92,9 @@ describe('agent policy', () => { revision: 1, monitoring_enabled: ['metrics'], }); - await agentPolicyService.bumpAllAgentPolicies(soClient); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + + await agentPolicyService.bumpAllAgentPolicies(soClient, esClient, undefined); expect(agentPolicyUpdateEventHandler).toHaveBeenCalledTimes(1); }); diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index 0fd41d074eff..81d98823b526 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -5,7 +5,12 @@ */ import { uniq } from 'lodash'; import { safeLoad } from 'js-yaml'; -import { SavedObjectsClientContract, SavedObjectsBulkUpdateResponse } from 'src/core/server'; +import uuid from 'uuid/v4'; +import { + ElasticsearchClient, + SavedObjectsClientContract, + SavedObjectsBulkUpdateResponse, +} from 'src/core/server'; import { AuthenticatedUser } from '../../../security/server'; import { DEFAULT_AGENT_POLICY, @@ -26,6 +31,8 @@ import { agentPolicyStatuses, storedPackagePoliciesToAgentInputs, dataTypes, + FleetServerPolicy, + AGENT_POLICY_INDEX, } from '../../common'; import { AgentPolicyNameExistsError } from '../errors'; import { createAgentPolicyAction, listAgents } from './agents'; @@ -36,20 +43,23 @@ import { getSettings } from './settings'; import { normalizeKuery, escapeSearchQueryPhrase } from './saved_object'; import { getFullAgentPolicyKibanaConfig } from '../../common/services/full_agent_policy_kibana_config'; import { isAgentsSetup } from './agents/setup'; +import { appContextService } from './app_context'; const SAVED_OBJECT_TYPE = AGENT_POLICY_SAVED_OBJECT_TYPE; class AgentPolicyService { private triggerAgentPolicyUpdatedEvent = async ( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, action: 'created' | 'updated' | 'deleted', agentPolicyId: string ) => { - return agentPolicyUpdateEventHandler(soClient, action, agentPolicyId); + return agentPolicyUpdateEventHandler(soClient, esClient, action, agentPolicyId); }; private async _update( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, id: string, agentPolicy: Partial, user?: AuthenticatedUser, @@ -78,14 +88,15 @@ class AgentPolicyService { }); if (options.bumpRevision) { - await this.triggerAgentPolicyUpdatedEvent(soClient, 'updated', id); + await this.triggerAgentPolicyUpdatedEvent(soClient, esClient, 'updated', id); } return (await this.get(soClient, id)) as AgentPolicy; } public async ensureDefaultAgentPolicy( - soClient: SavedObjectsClientContract + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient ): Promise<{ created: boolean; defaultAgentPolicy: AgentPolicy; @@ -103,7 +114,7 @@ class AgentPolicyService { return { created: true, - defaultAgentPolicy: await this.create(soClient, newDefaultAgentPolicy), + defaultAgentPolicy: await this.create(soClient, esClient, newDefaultAgentPolicy), }; } @@ -118,6 +129,7 @@ class AgentPolicyService { public async create( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agentPolicy: NewAgentPolicy, options?: { id?: string; user?: AuthenticatedUser } ): Promise { @@ -134,7 +146,7 @@ class AgentPolicyService { ); if (!agentPolicy.is_default) { - await this.triggerAgentPolicyUpdatedEvent(soClient, 'created', newSo.id); + await this.triggerAgentPolicyUpdatedEvent(soClient, esClient, 'created', newSo.id); } return { id: newSo.id, ...newSo.attributes }; @@ -244,6 +256,7 @@ class AgentPolicyService { public async update( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, id: string, agentPolicy: Partial, options?: { user?: AuthenticatedUser } @@ -254,11 +267,12 @@ class AgentPolicyService { name: agentPolicy.name, }); } - return this._update(soClient, id, agentPolicy, options?.user); + return this._update(soClient, esClient, id, agentPolicy, options?.user); } public async copy( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, id: string, newAgentPolicyProps: Pick, options?: { user?: AuthenticatedUser } @@ -272,6 +286,7 @@ class AgentPolicyService { const { namespace, monitoring_enabled } = baseAgentPolicy; const newAgentPolicy = await this.create( soClient, + esClient, { namespace, monitoring_enabled, @@ -288,10 +303,16 @@ class AgentPolicyService { return newPackagePolicy; } ); - await packagePolicyService.bulkCreate(soClient, newPackagePolicies, newAgentPolicy.id, { - ...options, - bumpRevision: false, - }); + await packagePolicyService.bulkCreate( + soClient, + esClient, + newPackagePolicies, + newAgentPolicy.id, + { + ...options, + bumpRevision: false, + } + ); } // Get updated agent policy @@ -307,15 +328,18 @@ class AgentPolicyService { public async bumpRevision( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, id: string, options?: { user?: AuthenticatedUser } ): Promise { - const res = await this._update(soClient, id, {}, options?.user); + const res = await this._update(soClient, esClient, id, {}, options?.user); return res; } + public async bumpAllAgentPolicies( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, options?: { user?: AuthenticatedUser } ): Promise>> { const currentPolicies = await soClient.find({ @@ -335,7 +359,7 @@ class AgentPolicyService { await Promise.all( currentPolicies.saved_objects.map((policy) => - this.triggerAgentPolicyUpdatedEvent(soClient, 'updated', policy.id) + this.triggerAgentPolicyUpdatedEvent(soClient, esClient, 'updated', policy.id) ) ); @@ -344,6 +368,7 @@ class AgentPolicyService { public async assignPackagePolicies( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, id: string, packagePolicyIds: string[], options: { user?: AuthenticatedUser; bumpRevision: boolean } = { bumpRevision: true } @@ -356,6 +381,7 @@ class AgentPolicyService { return await this._update( soClient, + esClient, id, { package_policies: uniq( @@ -369,6 +395,7 @@ class AgentPolicyService { public async unassignPackagePolicies( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, id: string, packagePolicyIds: string[], options?: { user?: AuthenticatedUser } @@ -381,6 +408,7 @@ class AgentPolicyService { return await this._update( soClient, + esClient, id, { package_policies: uniq( @@ -409,6 +437,7 @@ class AgentPolicyService { public async delete( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, id: string ): Promise { const agentPolicy = await this.get(soClient, id, false); @@ -418,12 +447,12 @@ class AgentPolicyService { const { defaultAgentPolicy: { id: defaultAgentPolicyId }, - } = await this.ensureDefaultAgentPolicy(soClient); + } = await this.ensureDefaultAgentPolicy(soClient, esClient); if (id === defaultAgentPolicyId) { throw new Error('The default agent policy cannot be deleted'); } - const { total } = await listAgents(soClient, { + const { total } = await listAgents(soClient, esClient, { showInactive: false, perPage: 0, page: 1, @@ -435,12 +464,17 @@ class AgentPolicyService { } if (agentPolicy.package_policies && agentPolicy.package_policies.length) { - await packagePolicyService.delete(soClient, agentPolicy.package_policies as string[], { - skipUnassignFromAgentPolicies: true, - }); + await packagePolicyService.delete( + soClient, + esClient, + agentPolicy.package_policies as string[], + { + skipUnassignFromAgentPolicies: true, + } + ); } await soClient.delete(SAVED_OBJECT_TYPE, id); - await this.triggerAgentPolicyUpdatedEvent(soClient, 'deleted', id); + await this.triggerAgentPolicyUpdatedEvent(soClient, esClient, 'deleted', id); return { id, name: agentPolicy.name, @@ -450,6 +484,19 @@ class AgentPolicyService { public async createFleetPolicyChangeAction( soClient: SavedObjectsClientContract, agentPolicyId: string + ) { + return appContextService.getConfig()?.agents.fleetServerEnabled + ? this.createFleetPolicyChangeFleetServer( + soClient, + appContextService.getInternalUserESClient(), + agentPolicyId + ) + : this.createFleetPolicyChangeActionSO(soClient, agentPolicyId); + } + + public async createFleetPolicyChangeActionSO( + soClient: SavedObjectsClientContract, + agentPolicyId: string ) { // If Agents is not setup skip the creation of POLICY_CHANGE agent actions // the action will be created during the fleet setup @@ -478,6 +525,38 @@ class AgentPolicyService { }); } + public async createFleetPolicyChangeFleetServer( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + agentPolicyId: string + ) { + // If Agents is not setup skip the creation of POLICY_CHANGE agent actions + // the action will be created during the fleet setup + if (!(await isAgentsSetup(soClient))) { + return; + } + const policy = await agentPolicyService.getFullAgentPolicy(soClient, agentPolicyId); + if (!policy || !policy.revision) { + return; + } + + const fleetServerPolicy: FleetServerPolicy = { + '@timestamp': new Date().toISOString(), + revision_idx: policy.revision, + coordinator_idx: 0, + data: (policy as unknown) as FleetServerPolicy['data'], + policy_id: policy.id, + default_fleet_server: false, + }; + + await esClient.create({ + index: AGENT_POLICY_INDEX, + body: fleetServerPolicy, + id: uuid(), + refresh: 'wait_for', + }); + } + public async getFullAgentPolicy( soClient: SavedObjectsClientContract, id: string, diff --git a/x-pack/plugins/fleet/server/services/agent_policy_update.ts b/x-pack/plugins/fleet/server/services/agent_policy_update.ts index fe06de765bbf..32c041b44681 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy_update.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy_update.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaRequest, SavedObjectsClientContract } from 'src/core/server'; +import { ElasticsearchClient, KibanaRequest, SavedObjectsClientContract } from 'src/core/server'; import { generateEnrollmentAPIKey, deleteEnrollmentApiKeyForAgentPolicyId } from './api_keys'; import { isAgentsSetup, unenrollForAgentPolicyId } from './agents'; import { agentPolicyService } from './agent_policy'; @@ -27,6 +27,7 @@ const fakeRequest = ({ export async function agentPolicyUpdateEventHandler( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, action: string, agentPolicyId: string ) { @@ -40,7 +41,7 @@ export async function agentPolicyUpdateEventHandler( const internalSoClient = appContextService.getInternalUserSOClient(fakeRequest); if (action === 'created') { - await generateEnrollmentAPIKey(soClient, { + await generateEnrollmentAPIKey(soClient, esClient, { agentPolicyId, }); await agentPolicyService.createFleetPolicyChangeAction(internalSoClient, agentPolicyId); @@ -51,7 +52,7 @@ export async function agentPolicyUpdateEventHandler( } if (action === 'deleted') { - await unenrollForAgentPolicyId(soClient, agentPolicyId); + await unenrollForAgentPolicyId(soClient, esClient, agentPolicyId); await deleteEnrollmentApiKeyForAgentPolicyId(soClient, agentPolicyId); } } diff --git a/x-pack/plugins/fleet/server/services/agents/acks.test.ts b/x-pack/plugins/fleet/server/services/agents/acks.test.ts index 4b09fb93e01a..1626df4fd02c 100644 --- a/x-pack/plugins/fleet/server/services/agents/acks.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/acks.test.ts @@ -5,7 +5,7 @@ */ import Boom from '@hapi/boom'; import { SavedObjectsBulkResponse } from 'kibana/server'; -import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { elasticsearchServiceMock, savedObjectsClientMock } from 'src/core/server/mocks'; import { Agent, @@ -19,6 +19,7 @@ import { acknowledgeAgentActions } from './acks'; describe('test agent acks services', () => { it('should succeed on valid and matched actions', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); + const mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; mockSavedObjectsClient.bulkGet.mockReturnValue( Promise.resolve({ @@ -41,6 +42,7 @@ describe('test agent acks services', () => { await acknowledgeAgentActions( mockSavedObjectsClient, + mockElasticsearchClient, ({ id: 'id', type: AGENT_TYPE_PERMANENT, @@ -59,6 +61,7 @@ describe('test agent acks services', () => { it('should update config field on the agent if a policy change is acknowledged with an agent without policy', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); + const mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; const actionAttributes = { type: 'POLICY_CHANGE', @@ -85,6 +88,7 @@ describe('test agent acks services', () => { await acknowledgeAgentActions( mockSavedObjectsClient, + mockElasticsearchClient, ({ id: 'id', type: AGENT_TYPE_PERMANENT, @@ -118,6 +122,7 @@ describe('test agent acks services', () => { it('should update config field on the agent if a policy change is acknowledged with a higher revision than the agent one', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); + const mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; const actionAttributes = { type: 'POLICY_CHANGE', @@ -144,6 +149,7 @@ describe('test agent acks services', () => { await acknowledgeAgentActions( mockSavedObjectsClient, + mockElasticsearchClient, ({ id: 'id', type: AGENT_TYPE_PERMANENT, @@ -178,6 +184,7 @@ describe('test agent acks services', () => { it('should not update config field on the agent if a policy change is acknowledged with a lower revision than the agent one', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); + const mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; const actionAttributes = { type: 'POLICY_CHANGE', @@ -204,6 +211,7 @@ describe('test agent acks services', () => { await acknowledgeAgentActions( mockSavedObjectsClient, + mockElasticsearchClient, ({ id: 'id', type: AGENT_TYPE_PERMANENT, @@ -226,6 +234,7 @@ describe('test agent acks services', () => { it('should not update config field on the agent if a policy change for an old revision is acknowledged', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); + const mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; mockSavedObjectsClient.bulkGet.mockReturnValue( Promise.resolve({ @@ -249,6 +258,7 @@ describe('test agent acks services', () => { await acknowledgeAgentActions( mockSavedObjectsClient, + mockElasticsearchClient, ({ id: 'id', type: AGENT_TYPE_PERMANENT, @@ -271,6 +281,7 @@ describe('test agent acks services', () => { it('should fail for actions that cannot be found on agent actions list', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); + const mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; mockSavedObjectsClient.bulkGet.mockReturnValue( Promise.resolve({ saved_objects: [ @@ -288,6 +299,7 @@ describe('test agent acks services', () => { try { await acknowledgeAgentActions( mockSavedObjectsClient, + mockElasticsearchClient, ({ id: 'id', type: AGENT_TYPE_PERMANENT, @@ -310,6 +322,7 @@ describe('test agent acks services', () => { it('should fail for events that have types not in the allowed acknowledgement type list', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); + const mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; mockSavedObjectsClient.bulkGet.mockReturnValue( Promise.resolve({ @@ -333,6 +346,7 @@ describe('test agent acks services', () => { try { await acknowledgeAgentActions( mockSavedObjectsClient, + mockElasticsearchClient, ({ id: 'id', type: AGENT_TYPE_PERMANENT, diff --git a/x-pack/plugins/fleet/server/services/agents/acks.ts b/x-pack/plugins/fleet/server/services/agents/acks.ts index 814251345788..fab6dae0d23d 100644 --- a/x-pack/plugins/fleet/server/services/agents/acks.ts +++ b/x-pack/plugins/fleet/server/services/agents/acks.ts @@ -5,6 +5,7 @@ */ import { + ElasticsearchClient, KibanaRequest, SavedObjectsBulkCreateObject, SavedObjectsBulkResponse, @@ -40,6 +41,7 @@ const actionCache = new LRU({ export async function acknowledgeAgentActions( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agent: Agent, agentEvents: AgentEvent[] ): Promise { @@ -79,7 +81,7 @@ export async function acknowledgeAgentActions( const isAgentUnenrolled = actions.some((action) => action.type === 'UNENROLL'); if (isAgentUnenrolled) { - await forceUnenrollAgent(soClient, agent.id); + await forceUnenrollAgent(soClient, esClient, agent.id); } const upgradeAction = actions.find((action) => action.type === 'UPGRADE'); @@ -196,6 +198,7 @@ export async function saveAgentEvents( export interface AcksService { acknowledgeAgentActions: ( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agent: Agent, actionIds: AgentEvent[] ) => Promise; @@ -207,6 +210,8 @@ export interface AcksService { getSavedObjectsClientContract: (kibanaRequest: KibanaRequest) => SavedObjectsClientContract; + getElasticsearchClientContract: () => ElasticsearchClient; + saveAgentEvents: ( soClient: SavedObjectsClientContract, events: AgentEvent[] diff --git a/x-pack/plugins/fleet/server/services/agents/actions.ts b/x-pack/plugins/fleet/server/services/agents/actions.ts index f2cdd1f31e69..cb893a8b88c9 100644 --- a/x-pack/plugins/fleet/server/services/agents/actions.ts +++ b/x-pack/plugins/fleet/server/services/agents/actions.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from 'kibana/server'; +import { ElasticsearchClient, SavedObjectsClientContract } from 'kibana/server'; import { Agent, AgentAction, @@ -307,7 +307,11 @@ export async function getLatestConfigChangeAction( } export interface ActionsService { - getAgent: (soClient: SavedObjectsClientContract, agentId: string) => Promise; + getAgent: ( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + agentId: string + ) => Promise; createAgentAction: ( soClient: SavedObjectsClientContract, diff --git a/x-pack/plugins/fleet/server/services/agents/checkin/index.ts b/x-pack/plugins/fleet/server/services/agents/checkin/index.ts index 19a5c2dc0876..d9e7d9889efd 100644 --- a/x-pack/plugins/fleet/server/services/agents/checkin/index.ts +++ b/x-pack/plugins/fleet/server/services/agents/checkin/index.ts @@ -5,7 +5,11 @@ */ import deepEqual from 'fast-deep-equal'; -import { SavedObjectsClientContract, SavedObjectsBulkCreateObject } from 'src/core/server'; +import { + ElasticsearchClient, + SavedObjectsClientContract, + SavedObjectsBulkCreateObject, +} from 'src/core/server'; import { Agent, NewAgentEvent, @@ -20,6 +24,7 @@ import { getAgentActionsForCheckin } from '../actions'; export async function agentCheckin( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agent: Agent, data: { events: NewAgentEvent[]; @@ -54,7 +59,7 @@ export async function agentCheckin( } // Wait for new actions - actions = await agentCheckinState.subscribeToNewActions(soClient, agent, options); + actions = await agentCheckinState.subscribeToNewActions(soClient, esClient, agent, options); return { actions }; } diff --git a/x-pack/plugins/fleet/server/services/agents/checkin/state.ts b/x-pack/plugins/fleet/server/services/agents/checkin/state.ts index 63f22b82611c..bdbf391650bc 100644 --- a/x-pack/plugins/fleet/server/services/agents/checkin/state.ts +++ b/x-pack/plugins/fleet/server/services/agents/checkin/state.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from 'src/core/server'; +import { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; import { Agent } from '../../../types'; import { appContextService } from '../../app_context'; import { agentCheckinStateConnectedAgentsFactory } from './state_connected_agents'; @@ -35,6 +35,7 @@ function agentCheckinStateFactory() { return { subscribeToNewActions: async ( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agent: Agent, options?: { signal: AbortSignal } ) => { @@ -44,7 +45,7 @@ function agentCheckinStateFactory() { return agentConnected.wrapPromise( agent.id, - newActions.subscribeToNewActions(soClient, agent, options) + newActions.subscribeToNewActions(soClient, esClient, agent, options) ); }, start, diff --git a/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.ts b/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.ts index 59887d223371..0d5394a88a87 100644 --- a/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.ts +++ b/x-pack/plugins/fleet/server/services/agents/checkin/state_new_actions.ts @@ -21,7 +21,7 @@ import { timeout, take, } from 'rxjs/operators'; -import { SavedObjectsClientContract, KibanaRequest } from 'src/core/server'; +import { ElasticsearchClient, SavedObjectsClientContract, KibanaRequest } from 'src/core/server'; import { Agent, AgentAction, @@ -228,6 +228,7 @@ export function agentCheckinStateNewActionsFactory() { async function subscribeToNewActions( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agent: Agent, options?: { signal: AbortSignal } ): Promise { @@ -262,7 +263,7 @@ export function agentCheckinStateNewActionsFactory() { (action) => action.type === 'INTERNAL_POLICY_REASSIGN' ); if (hasConfigReassign) { - return from(getAgent(soClient, agent.id)).pipe( + return from(getAgent(soClient, esClient, agent.id)).pipe( concatMap((refreshedAgent) => { if (!refreshedAgent.policy_id) { throw new Error('Agent does not have a policy assigned'); diff --git a/x-pack/plugins/fleet/server/services/agents/crud.ts b/x-pack/plugins/fleet/server/services/agents/crud.ts index bcd409e5f7ea..58f64c65e081 100644 --- a/x-pack/plugins/fleet/server/services/agents/crud.ts +++ b/x-pack/plugins/fleet/server/services/agents/crud.ts @@ -4,29 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ import Boom from '@hapi/boom'; -import { SavedObjectsClientContract } from 'src/core/server'; -import { isAgentUpgradeable } from '../../../common'; -import { AGENT_SAVED_OBJECT_TYPE, AGENT_EVENT_SAVED_OBJECT_TYPE } from '../../constants'; -import { AgentSOAttributes, Agent, AgentEventSOAttributes, ListWithKuery } from '../../types'; -import { escapeSearchQueryPhrase, normalizeKuery, findAllSOs } from '../saved_object'; +import { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; + +import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; +import { AgentSOAttributes, Agent, ListWithKuery } from '../../types'; +import { escapeSearchQueryPhrase } from '../saved_object'; import { savedObjectToAgent } from './saved_objects'; import { appContextService } from '../../services'; - -const ACTIVE_AGENT_CONDITION = `${AGENT_SAVED_OBJECT_TYPE}.attributes.active:true`; -const INACTIVE_AGENT_CONDITION = `NOT (${ACTIVE_AGENT_CONDITION})`; - -function _joinFilters(filters: string[], operator = 'AND') { - return filters.reduce((acc: string | undefined, filter) => { - if (acc) { - return `${acc} ${operator} (${filter})`; - } - - return `(${filter})`; - }, undefined); -} +import * as crudServiceSO from './crud_so'; +import * as crudServiceFleetServer from './crud_fleet_server'; export async function listAgents( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, options: ListWithKuery & { showInactive: boolean; } @@ -36,52 +26,16 @@ export async function listAgents( page: number; perPage: number; }> { - const { - page = 1, - perPage = 20, - sortField = 'enrolled_at', - sortOrder = 'desc', - kuery, - showInactive = false, - showUpgradeable, - } = options; - const filters = []; + const fleetServerEnabled = appContextService.getConfig()?.agents?.fleetServerEnabled; - if (kuery && kuery !== '') { - filters.push(normalizeKuery(AGENT_SAVED_OBJECT_TYPE, kuery)); - } - - if (showInactive === false) { - filters.push(ACTIVE_AGENT_CONDITION); - } - - let { saved_objects: agentSOs, total } = await soClient.find({ - type: AGENT_SAVED_OBJECT_TYPE, - filter: _joinFilters(filters), - sortField, - sortOrder, - page, - perPage, - }); - // filtering for a range on the version string will not work, - // nor does filtering on a flattened field (local_metadata), so filter here - if (showUpgradeable) { - agentSOs = agentSOs.filter((agent) => - isAgentUpgradeable(savedObjectToAgent(agent), appContextService.getKibanaVersion()) - ); - total = agentSOs.length; - } - - return { - agents: agentSOs.map(savedObjectToAgent), - total, - page, - perPage, - }; + return fleetServerEnabled + ? crudServiceFleetServer.listAgents(esClient, options) + : crudServiceSO.listAgents(soClient, options); } export async function listAllAgents( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, options: Omit & { showInactive: boolean; } @@ -89,55 +43,34 @@ export async function listAllAgents( agents: Agent[]; total: number; }> { - const { sortField = 'enrolled_at', sortOrder = 'desc', kuery, showInactive = false } = options; - const filters = []; + const fleetServerEnabled = appContextService.getConfig()?.agents?.fleetServerEnabled; - if (kuery && kuery !== '') { - filters.push(normalizeKuery(AGENT_SAVED_OBJECT_TYPE, kuery)); - } - - if (showInactive === false) { - filters.push(ACTIVE_AGENT_CONDITION); - } - - const { saved_objects: agentSOs, total } = await findAllSOs(soClient, { - type: AGENT_SAVED_OBJECT_TYPE, - kuery: _joinFilters(filters), - sortField, - sortOrder, - }); - - return { - agents: agentSOs.map(savedObjectToAgent), - total, - }; + return fleetServerEnabled + ? crudServiceFleetServer.listAllAgents(esClient, options) + : crudServiceSO.listAllAgents(soClient, options); } export async function countInactiveAgents( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, options: Pick ): Promise { - const { kuery } = options; - const filters = [INACTIVE_AGENT_CONDITION]; + const fleetServerEnabled = appContextService.getConfig()?.agents?.fleetServerEnabled; - if (kuery && kuery !== '') { - filters.push(normalizeKuery(AGENT_SAVED_OBJECT_TYPE, kuery)); - } - - const { total } = await soClient.find({ - type: AGENT_SAVED_OBJECT_TYPE, - filter: _joinFilters(filters), - perPage: 0, - }); - - return total; + return fleetServerEnabled + ? crudServiceFleetServer.countInactiveAgents(esClient, options) + : crudServiceSO.countInactiveAgents(soClient, options); } -export async function getAgent(soClient: SavedObjectsClientContract, agentId: string) { - const agent = savedObjectToAgent( - await soClient.get(AGENT_SAVED_OBJECT_TYPE, agentId) - ); - return agent; +export async function getAgent( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + agentId: string +) { + const fleetServerEnabled = appContextService.getConfig()?.agents?.fleetServerEnabled; + return fleetServerEnabled + ? crudServiceFleetServer.getAgent(esClient, agentId) + : crudServiceSO.getAgent(soClient, agentId); } export async function getAgents(soClient: SavedObjectsClientContract, agentIds: string[]) { @@ -187,31 +120,13 @@ export async function updateAgent( }); } -export async function deleteAgent(soClient: SavedObjectsClientContract, agentId: string) { - const agent = await getAgent(soClient, agentId); - if (agent.type === 'EPHEMERAL') { - // Delete events - let more = true; - while (more === true) { - const { saved_objects: events } = await soClient.find({ - type: AGENT_EVENT_SAVED_OBJECT_TYPE, - fields: ['id'], - search: agentId, - searchFields: ['agent_id'], - perPage: 1000, - }); - if (events.length === 0) { - more = false; - } - for (const event of events) { - await soClient.delete(AGENT_EVENT_SAVED_OBJECT_TYPE, event.id); - } - } - await soClient.delete(AGENT_SAVED_OBJECT_TYPE, agentId); - return; - } - - await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { - active: false, - }); +export async function deleteAgent( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + agentId: string +) { + const fleetServerEnabled = appContextService.getConfig()?.agents?.fleetServerEnabled; + return fleetServerEnabled + ? crudServiceFleetServer.deleteAgent(esClient, agentId) + : crudServiceSO.deleteAgent(soClient, agentId); } diff --git a/x-pack/plugins/fleet/server/services/agents/crud_fleet_server.ts b/x-pack/plugins/fleet/server/services/agents/crud_fleet_server.ts new file mode 100644 index 000000000000..9c5e45c05de0 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/agents/crud_fleet_server.ts @@ -0,0 +1,197 @@ +/* + * 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 Boom from '@hapi/boom'; +import { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; + +import { isAgentUpgradeable, SO_SEARCH_LIMIT } from '../../../common'; +import { AGENT_SAVED_OBJECT_TYPE, AGENTS_INDEX } from '../../constants'; +import { ESSearchHit } from '../../../../../typings/elasticsearch'; +import { AgentSOAttributes, Agent, ListWithKuery } from '../../types'; +import { escapeSearchQueryPhrase, normalizeKuery } from '../saved_object'; +import { savedObjectToAgent } from './saved_objects'; +import { searchHitToAgent } from './helpers'; +import { appContextService } from '../../services'; + +const ACTIVE_AGENT_CONDITION = 'active:true'; +const INACTIVE_AGENT_CONDITION = `NOT (${ACTIVE_AGENT_CONDITION})`; + +function _joinFilters(filters: string[], operator = 'AND') { + return filters.reduce((acc: string | undefined, filter) => { + if (acc) { + return `${acc} ${operator} (${filter})`; + } + + return `(${filter})`; + }, undefined); +} + +function removeSOAttributes(kuery: string) { + return kuery.replace(/attributes\./g, '').replace(/fleet-agents\./g, ''); +} + +export async function listAgents( + esClient: ElasticsearchClient, + options: ListWithKuery & { + showInactive: boolean; + } +): Promise<{ + agents: Agent[]; + total: number; + page: number; + perPage: number; +}> { + const { + page = 1, + perPage = 20, + sortField = 'enrolled_at', + sortOrder = 'desc', + kuery, + showInactive = false, + showUpgradeable, + } = options; + const filters = []; + + if (kuery && kuery !== '') { + filters.push(removeSOAttributes(kuery)); + } + + if (showInactive === false) { + filters.push(ACTIVE_AGENT_CONDITION); + } + + const res = await esClient.search({ + index: AGENTS_INDEX, + from: (page - 1) * perPage, + size: perPage, + sort: `${sortField}:${sortOrder}`, + track_total_hits: true, + q: _joinFilters(filters), + }); + + let agentResults: Agent[] = res.body.hits.hits.map(searchHitToAgent); + let total = res.body.hits.total.value; + + // filtering for a range on the version string will not work, + // nor does filtering on a flattened field (local_metadata), so filter here + if (showUpgradeable) { + agentResults = agentResults.filter((agent) => + isAgentUpgradeable(agent, appContextService.getKibanaVersion()) + ); + total = agentResults.length; + } + + return { + agents: res.body.hits.hits.map(searchHitToAgent), + total, + page, + perPage, + }; +} + +export async function listAllAgents( + esClient: ElasticsearchClient, + options: Omit & { + showInactive: boolean; + } +): Promise<{ + agents: Agent[]; + total: number; +}> { + const res = await listAgents(esClient, { ...options, page: 1, perPage: SO_SEARCH_LIMIT }); + + return { + agents: res.agents, + total: res.total, + }; +} + +export async function countInactiveAgents( + esClient: ElasticsearchClient, + options: Pick +): Promise { + const { kuery } = options; + const filters = [INACTIVE_AGENT_CONDITION]; + + if (kuery && kuery !== '') { + filters.push(normalizeKuery(AGENT_SAVED_OBJECT_TYPE, kuery)); + } + + const res = await esClient.search({ + index: AGENTS_INDEX, + size: 0, + track_total_hits: true, + q: _joinFilters(filters), + }); + + return res.body.hits.total.value; +} + +export async function getAgent(esClient: ElasticsearchClient, agentId: string) { + const agentHit = await esClient.get>({ + index: AGENTS_INDEX, + id: agentId, + }); + const agent = searchHitToAgent(agentHit.body); + + return agent; +} + +export async function getAgents(soClient: SavedObjectsClientContract, agentIds: string[]) { + const agentSOs = await soClient.bulkGet( + agentIds.map((agentId) => ({ + id: agentId, + type: AGENT_SAVED_OBJECT_TYPE, + })) + ); + const agents = agentSOs.saved_objects.map(savedObjectToAgent); + return agents; +} + +export async function getAgentByAccessAPIKeyId( + soClient: SavedObjectsClientContract, + accessAPIKeyId: string +): Promise { + const response = await soClient.find({ + type: AGENT_SAVED_OBJECT_TYPE, + searchFields: ['access_api_key_id'], + search: escapeSearchQueryPhrase(accessAPIKeyId), + }); + const [agent] = response.saved_objects.map(savedObjectToAgent); + + if (!agent) { + throw Boom.notFound('Agent not found'); + } + if (agent.access_api_key_id !== accessAPIKeyId) { + throw new Error('Agent api key id is not matching'); + } + if (!agent.active) { + throw Boom.forbidden('Agent inactive'); + } + + return agent; +} + +export async function updateAgent( + soClient: SavedObjectsClientContract, + agentId: string, + data: { + userProvidedMetatada: any; + } +) { + await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { + user_provided_metadata: data.userProvidedMetatada, + }); +} + +export async function deleteAgent(esClient: ElasticsearchClient, agentId: string) { + await esClient.update({ + id: agentId, + index: AGENT_SAVED_OBJECT_TYPE, + body: { + active: false, + }, + }); +} diff --git a/x-pack/plugins/fleet/server/services/agents/crud_so.ts b/x-pack/plugins/fleet/server/services/agents/crud_so.ts new file mode 100644 index 000000000000..eb8f389741a6 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/agents/crud_so.ts @@ -0,0 +1,195 @@ +/* + * 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 Boom from '@hapi/boom'; +import { SavedObjectsClientContract } from 'src/core/server'; + +import { isAgentUpgradeable } from '../../../common'; +import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; +import { AgentSOAttributes, Agent, ListWithKuery } from '../../types'; +import { escapeSearchQueryPhrase, normalizeKuery, findAllSOs } from '../saved_object'; +import { savedObjectToAgent } from './saved_objects'; +import { appContextService } from '../../services'; + +const ACTIVE_AGENT_CONDITION = `${AGENT_SAVED_OBJECT_TYPE}.attributes.active:true`; +const INACTIVE_AGENT_CONDITION = `NOT (${ACTIVE_AGENT_CONDITION})`; + +function _joinFilters(filters: string[], operator = 'AND') { + return filters.reduce((acc: string | undefined, filter) => { + if (acc) { + return `${acc} ${operator} (${filter})`; + } + + return `(${filter})`; + }, undefined); +} + +export async function listAgents( + soClient: SavedObjectsClientContract, + options: ListWithKuery & { + showInactive: boolean; + } +): Promise<{ + agents: Agent[]; + total: number; + page: number; + perPage: number; +}> { + const { + page = 1, + perPage = 20, + sortField = 'enrolled_at', + sortOrder = 'desc', + kuery, + showInactive = false, + showUpgradeable, + } = options; + const filters = []; + + if (kuery && kuery !== '') { + filters.push(normalizeKuery(AGENT_SAVED_OBJECT_TYPE, kuery)); + } + + if (showInactive === false) { + filters.push(ACTIVE_AGENT_CONDITION); + } + + let { saved_objects: agentSOs, total } = await soClient.find({ + type: AGENT_SAVED_OBJECT_TYPE, + filter: _joinFilters(filters), + sortField, + sortOrder, + page, + perPage, + }); + // filtering for a range on the version string will not work, + // nor does filtering on a flattened field (local_metadata), so filter here + if (showUpgradeable) { + agentSOs = agentSOs.filter((agent) => + isAgentUpgradeable(savedObjectToAgent(agent), appContextService.getKibanaVersion()) + ); + total = agentSOs.length; + } + + return { + agents: agentSOs.map(savedObjectToAgent), + total, + page, + perPage, + }; +} + +export async function listAllAgents( + soClient: SavedObjectsClientContract, + options: Omit & { + showInactive: boolean; + } +): Promise<{ + agents: Agent[]; + total: number; +}> { + const { sortField = 'enrolled_at', sortOrder = 'desc', kuery, showInactive = false } = options; + const filters = []; + + if (kuery && kuery !== '') { + filters.push(normalizeKuery(AGENT_SAVED_OBJECT_TYPE, kuery)); + } + + if (showInactive === false) { + filters.push(ACTIVE_AGENT_CONDITION); + } + + const { saved_objects: agentSOs, total } = await findAllSOs(soClient, { + type: AGENT_SAVED_OBJECT_TYPE, + kuery: _joinFilters(filters), + sortField, + sortOrder, + }); + + return { + agents: agentSOs.map(savedObjectToAgent), + total, + }; +} + +export async function countInactiveAgents( + soClient: SavedObjectsClientContract, + options: Pick +): Promise { + const { kuery } = options; + const filters = [INACTIVE_AGENT_CONDITION]; + + if (kuery && kuery !== '') { + filters.push(normalizeKuery(AGENT_SAVED_OBJECT_TYPE, kuery)); + } + + const { total } = await soClient.find({ + type: AGENT_SAVED_OBJECT_TYPE, + filter: _joinFilters(filters), + perPage: 0, + }); + + return total; +} + +export async function getAgent(soClient: SavedObjectsClientContract, agentId: string) { + const agent = savedObjectToAgent( + await soClient.get(AGENT_SAVED_OBJECT_TYPE, agentId) + ); + return agent; +} + +export async function getAgents(soClient: SavedObjectsClientContract, agentIds: string[]) { + const agentSOs = await soClient.bulkGet( + agentIds.map((agentId) => ({ + id: agentId, + type: AGENT_SAVED_OBJECT_TYPE, + })) + ); + const agents = agentSOs.saved_objects.map(savedObjectToAgent); + return agents; +} + +export async function getAgentByAccessAPIKeyId( + soClient: SavedObjectsClientContract, + accessAPIKeyId: string +): Promise { + const response = await soClient.find({ + type: AGENT_SAVED_OBJECT_TYPE, + searchFields: ['access_api_key_id'], + search: escapeSearchQueryPhrase(accessAPIKeyId), + }); + const [agent] = response.saved_objects.map(savedObjectToAgent); + + if (!agent) { + throw Boom.notFound('Agent not found'); + } + if (agent.access_api_key_id !== accessAPIKeyId) { + throw new Error('Agent api key id is not matching'); + } + if (!agent.active) { + throw Boom.forbidden('Agent inactive'); + } + + return agent; +} + +export async function updateAgent( + soClient: SavedObjectsClientContract, + agentId: string, + data: { + userProvidedMetatada: any; + } +) { + await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { + user_provided_metadata: data.userProvidedMetatada, + }); +} + +export async function deleteAgent(soClient: SavedObjectsClientContract, agentId: string) { + await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { + active: false, + }); +} diff --git a/x-pack/plugins/fleet/server/services/agents/helpers.ts b/x-pack/plugins/fleet/server/services/agents/helpers.ts new file mode 100644 index 000000000000..38330a090ae8 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/agents/helpers.ts @@ -0,0 +1,21 @@ +/* + * 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 { ESSearchHit } from '../../../../../typings/elasticsearch'; +import { Agent, AgentSOAttributes } from '../../types'; + +export function searchHitToAgent(hit: ESSearchHit): Agent { + return { + id: hit._id, + ...hit._source, + current_error_events: hit._source.current_error_events + ? JSON.parse(hit._source.current_error_events) + : [], + access_api_key: undefined, + status: undefined, + packages: hit._source.packages ?? [], + }; +} diff --git a/x-pack/plugins/fleet/server/services/agents/reassign.ts b/x-pack/plugins/fleet/server/services/agents/reassign.ts index b656ab12e96c..8a1dc6195088 100644 --- a/x-pack/plugins/fleet/server/services/agents/reassign.ts +++ b/x-pack/plugins/fleet/server/services/agents/reassign.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from 'kibana/server'; +import { SavedObjectsClientContract, ElasticsearchClient } from 'kibana/server'; import Boom from '@hapi/boom'; import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; import { AgentSOAttributes } from '../../types'; @@ -14,6 +14,7 @@ import { createAgentAction, bulkCreateAgentActions } from './actions'; export async function reassignAgent( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agentId: string, newAgentPolicyId: string ) { @@ -36,6 +37,7 @@ export async function reassignAgent( export async function reassignAgents( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, options: | { agentIds: string[]; @@ -55,7 +57,7 @@ export async function reassignAgents( 'agentIds' in options ? await getAgents(soClient, options.agentIds) : ( - await listAllAgents(soClient, { + await listAllAgents(soClient, esClient, { kuery: options.kuery, showInactive: false, }) diff --git a/x-pack/plugins/fleet/server/services/agents/status.test.ts b/x-pack/plugins/fleet/server/services/agents/status.test.ts index f216cd541eb2..587f0af227ff 100644 --- a/x-pack/plugins/fleet/server/services/agents/status.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/status.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { elasticsearchServiceMock, savedObjectsClientMock } from 'src/core/server/mocks'; import { getAgentStatusById } from './status'; import { AGENT_TYPE_PERMANENT } from '../../../common/constants'; import { AgentSOAttributes } from '../../../common/types/models'; @@ -13,6 +13,7 @@ import { SavedObject } from 'kibana/server'; describe('Agent status service', () => { it('should return inactive when agent is not active', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); + const mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; mockSavedObjectsClient.get = jest.fn().mockReturnValue({ id: 'id', type: AGENT_TYPE_PERMANENT, @@ -22,12 +23,13 @@ describe('Agent status service', () => { user_provided_metadata: {}, }, } as SavedObject); - const status = await getAgentStatusById(mockSavedObjectsClient, 'id'); + const status = await getAgentStatusById(mockSavedObjectsClient, mockElasticsearchClient, 'id'); expect(status).toEqual('inactive'); }); it('should return online when agent is active', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); + const mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; mockSavedObjectsClient.get = jest.fn().mockReturnValue({ id: 'id', type: AGENT_TYPE_PERMANENT, @@ -38,12 +40,13 @@ describe('Agent status service', () => { user_provided_metadata: {}, }, } as SavedObject); - const status = await getAgentStatusById(mockSavedObjectsClient, 'id'); + const status = await getAgentStatusById(mockSavedObjectsClient, mockElasticsearchClient, 'id'); expect(status).toEqual('online'); }); it('should return enrolling when agent is active but never checkin', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); + const mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; mockSavedObjectsClient.get = jest.fn().mockReturnValue({ id: 'id', type: AGENT_TYPE_PERMANENT, @@ -53,12 +56,13 @@ describe('Agent status service', () => { user_provided_metadata: {}, }, } as SavedObject); - const status = await getAgentStatusById(mockSavedObjectsClient, 'id'); + const status = await getAgentStatusById(mockSavedObjectsClient, mockElasticsearchClient, 'id'); expect(status).toEqual('enrolling'); }); it('should return unenrolling when agent is unenrolling', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); + const mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; mockSavedObjectsClient.get = jest.fn().mockReturnValue({ id: 'id', type: AGENT_TYPE_PERMANENT, @@ -70,7 +74,7 @@ describe('Agent status service', () => { user_provided_metadata: {}, }, } as SavedObject); - const status = await getAgentStatusById(mockSavedObjectsClient, 'id'); + const status = await getAgentStatusById(mockSavedObjectsClient, mockElasticsearchClient, 'id'); expect(status).toEqual('unenrolling'); }); }); diff --git a/x-pack/plugins/fleet/server/services/agents/status.ts b/x-pack/plugins/fleet/server/services/agents/status.ts index 74faedc8e293..ba8f8fc36385 100644 --- a/x-pack/plugins/fleet/server/services/agents/status.ts +++ b/x-pack/plugins/fleet/server/services/agents/status.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from 'src/core/server'; +import { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; import pMap from 'p-map'; import { getAgent, listAgents } from './crud'; import { AGENT_EVENT_SAVED_OBJECT_TYPE, AGENT_SAVED_OBJECT_TYPE } from '../../constants'; @@ -14,9 +14,10 @@ import { AgentStatusKueryHelper } from '../../../common/services'; export async function getAgentStatusById( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agentId: string ): Promise { - const agent = await getAgent(soClient, agentId); + const agent = await getAgent(soClient, esClient, agentId); return AgentStatusKueryHelper.getAgentStatus(agent); } @@ -36,6 +37,7 @@ function joinKuerys(...kuerys: Array) { export async function getAgentStatusForAgentPolicy( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, agentPolicyId?: string, filterKuery?: string ) { @@ -48,7 +50,7 @@ export async function getAgentStatusForAgentPolicy( AgentStatusKueryHelper.buildKueryForUpdatingAgents(), ], (kuery) => - listAgents(soClient, { + listAgents(soClient, esClient, { showInactive: false, perPage: 0, page: 1, diff --git a/x-pack/plugins/fleet/server/services/agents/unenroll.ts b/x-pack/plugins/fleet/server/services/agents/unenroll.ts index 9c2b2bdfe7f6..5246927cb4ee 100644 --- a/x-pack/plugins/fleet/server/services/agents/unenroll.ts +++ b/x-pack/plugins/fleet/server/services/agents/unenroll.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from 'src/core/server'; +import { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; import { AgentSOAttributes } from '../../types'; import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; import { getAgent } from './crud'; @@ -25,6 +25,7 @@ export async function unenrollAgent(soClient: SavedObjectsClientContract, agentI export async function unenrollAgents( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, options: | { agentIds: string[]; @@ -38,7 +39,7 @@ export async function unenrollAgents( 'agentIds' in options ? await getAgents(soClient, options.agentIds) : ( - await listAllAgents(soClient, { + await listAllAgents(soClient, esClient, { kuery: options.kuery, showInactive: false, }) @@ -70,8 +71,12 @@ export async function unenrollAgents( ); } -export async function forceUnenrollAgent(soClient: SavedObjectsClientContract, agentId: string) { - const agent = await getAgent(soClient, agentId); +export async function forceUnenrollAgent( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + agentId: string +) { + const agent = await getAgent(soClient, esClient, agentId); await Promise.all([ agent.access_api_key_id @@ -90,6 +95,7 @@ export async function forceUnenrollAgent(soClient: SavedObjectsClientContract, a export async function forceUnenrollAgents( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, options: | { agentIds: string[]; @@ -103,7 +109,7 @@ export async function forceUnenrollAgents( 'agentIds' in options ? await getAgents(soClient, options.agentIds) : ( - await listAllAgents(soClient, { + await listAllAgents(soClient, esClient, { kuery: options.kuery, showInactive: false, }) diff --git a/x-pack/plugins/fleet/server/services/agents/update.ts b/x-pack/plugins/fleet/server/services/agents/update.ts index b85a831294b5..7bd807bf4e57 100644 --- a/x-pack/plugins/fleet/server/services/agents/update.ts +++ b/x-pack/plugins/fleet/server/services/agents/update.ts @@ -4,19 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from 'src/core/server'; +import { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; import { listAgents } from './crud'; import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; import { unenrollAgent } from './unenroll'; export async function unenrollForAgentPolicyId( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, policyId: string ) { let hasMore = true; let page = 1; while (hasMore) { - const { agents } = await listAgents(soClient, { + const { agents } = await listAgents(soClient, esClient, { kuery: `${AGENT_SAVED_OBJECT_TYPE}.policy_id:"${policyId}"`, page: page++, perPage: 1000, diff --git a/x-pack/plugins/fleet/server/services/agents/upgrade.ts b/x-pack/plugins/fleet/server/services/agents/upgrade.ts index cf83a938d3c3..9515cca8ce00 100644 --- a/x-pack/plugins/fleet/server/services/agents/upgrade.ts +++ b/x-pack/plugins/fleet/server/services/agents/upgrade.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from 'src/core/server'; +import { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; import { AgentSOAttributes, AgentAction, AgentActionSOAttributes } from '../../types'; import { AGENT_ACTION_SAVED_OBJECT_TYPE, AGENT_SAVED_OBJECT_TYPE } from '../../constants'; import { bulkCreateAgentActions, createAgentAction } from './actions'; @@ -59,6 +59,7 @@ export async function ackAgentUpgraded( export async function sendUpgradeAgentsActions( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, options: | { agentIds: string[]; @@ -79,7 +80,7 @@ export async function sendUpgradeAgentsActions( 'agentIds' in options ? await getAgents(soClient, options.agentIds) : ( - await listAllAgents(soClient, { + await listAllAgents(soClient, esClient, { kuery: options.kuery, showInactive: 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 8f67753392e6..747cbae3f71c 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, invalidateAPIKeys } 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 invalidateAPIKeys(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'); - } - - 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; + if (appContextService.getConfig()?.agents?.fleetServerEnabled === true) { + return enrollmentApiKeyServiceFleetServer.generateEnrollmentAPIKey(soClient, esClient, data); + } else { + return enrollmentApiKeyServiceSO.generateEnrollmentAPIKey(soClient, data); } } - -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 000000000000..c0aa42c6e4ed --- /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, invalidateAPIKeys } 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, + q: 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 invalidateAPIKeys(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 000000000000..8f67753392e6 --- /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, invalidateAPIKeys } 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 invalidateAPIKeys(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/app_context.ts b/x-pack/plugins/fleet/server/services/app_context.ts index d6b62458ed1f..66ffd3ca5308 100644 --- a/x-pack/plugins/fleet/server/services/app_context.ts +++ b/x-pack/plugins/fleet/server/services/app_context.ts @@ -5,7 +5,13 @@ */ import { BehaviorSubject, Observable } from 'rxjs'; import { first } from 'rxjs/operators'; -import { SavedObjectsServiceStart, HttpServiceSetup, Logger, KibanaRequest } from 'src/core/server'; +import { + ElasticsearchClient, + SavedObjectsServiceStart, + HttpServiceSetup, + Logger, + KibanaRequest, +} from 'src/core/server'; import { EncryptedSavedObjectsClient, EncryptedSavedObjectsPluginSetup, @@ -19,6 +25,7 @@ import { CloudSetup } from '../../../cloud/server'; class AppContextService { private encryptedSavedObjects: EncryptedSavedObjectsClient | undefined; private encryptedSavedObjectsSetup: EncryptedSavedObjectsPluginSetup | undefined; + private esClient: ElasticsearchClient | undefined; private security: SecurityPluginStart | undefined; private config$?: Observable; private configSubject$?: BehaviorSubject; @@ -32,6 +39,7 @@ class AppContextService { private externalCallbacks: ExternalCallbacksStorage = new Map(); public async start(appContext: FleetAppContext) { + this.esClient = appContext.elasticsearch.client.asInternalUser; this.encryptedSavedObjects = appContext.encryptedSavedObjectsStart?.getClient(); this.encryptedSavedObjectsSetup = appContext.encryptedSavedObjectsSetup; this.security = appContext.security; @@ -96,12 +104,20 @@ class AppContextService { } public getInternalUserSOClient(request: KibanaRequest) { - // soClient as kibana internal users, be carefull on how you use it, security is not enabled + // soClient as kibana internal users, be careful on how you use it, security is not enabled return appContextService.getSavedObjects().getScopedClient(request, { excludedWrappers: ['security'], }); } + public getInternalUserESClient() { + if (!this.esClient) { + throw new Error('Elasticsearch start service not set.'); + } + // soClient as kibana internal users, be careful on how you use it, security is not enabled + return this.esClient; + } + public getIsProductionMode() { return this.isProductionMode; } diff --git a/x-pack/plugins/fleet/server/services/fleet_server_migration.ts b/x-pack/plugins/fleet/server/services/fleet_server_migration.ts new file mode 100644 index 000000000000..1a50b5c9df76 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/fleet_server_migration.ts @@ -0,0 +1,75 @@ +/* + * 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 { KibanaRequest } from 'src/core/server'; +import { + ENROLLMENT_API_KEYS_INDEX, + ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, + FleetServerEnrollmentAPIKey, +} from '../../common'; +import { listEnrollmentApiKeys, getEnrollmentAPIKey } from './api_keys/enrollment_api_key_so'; +import { appContextService } from './app_context'; + +export async function runFleetServerMigration() { + const logger = appContextService.getLogger(); + logger.info('Starting fleet server migration'); + await migrateEnrollmentApiKeys(); + logger.info('Fleet server migration finished'); +} + +function getInternalUserSOClient() { + const fakeRequest = ({ + headers: {}, + getBasePath: () => '', + path: '/', + route: { settings: {} }, + url: { + href: '/', + }, + raw: { + req: { + url: '/', + }, + }, + } as unknown) as KibanaRequest; + + return appContextService.getInternalUserSOClient(fakeRequest); +} + +async function migrateEnrollmentApiKeys() { + const esClient = appContextService.getInternalUserESClient(); + const soClient = getInternalUserSOClient(); + let hasMore = true; + while (hasMore) { + const res = await listEnrollmentApiKeys(soClient, { + page: 1, + perPage: 100, + }); + if (res.total === 0) { + hasMore = false; + } + for (const item of res.items) { + const key = await getEnrollmentAPIKey(soClient, item.id); + + const body: FleetServerEnrollmentAPIKey = { + api_key: key.api_key, + api_key_id: key.api_key_id, + active: key.active, + created_at: key.created_at, + name: key.name, + policy_id: key.policy_id, + }; + await esClient.create({ + index: ENROLLMENT_API_KEYS_INDEX, + body, + id: key.id, + refresh: 'wait_for', + }); + + await soClient.delete(ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, key.id); + } + } +} diff --git a/x-pack/plugins/fleet/server/services/index.ts b/x-pack/plugins/fleet/server/services/index.ts index d9015c519553..b590b2ed002c 100644 --- a/x-pack/plugins/fleet/server/services/index.ts +++ b/x-pack/plugins/fleet/server/services/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract, KibanaRequest } from 'kibana/server'; +import { ElasticsearchClient, SavedObjectsClientContract, KibanaRequest } from 'kibana/server'; import { AgentStatus, Agent, EsAssetReference } from '../types'; import * as settingsService from './settings'; import { getAgent, listAgents } from './agents'; @@ -53,7 +53,11 @@ export interface AgentService { /** * Return the status by the Agent's id */ - getAgentStatusById(soClient: SavedObjectsClientContract, agentId: string): Promise; + getAgentStatusById( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + agentId: string + ): Promise; /** * List agents */ diff --git a/x-pack/plugins/fleet/server/services/package_policy.test.ts b/x-pack/plugins/fleet/server/services/package_policy.test.ts index 5e295c157670..eb26b405fbda 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { elasticsearchServiceMock, savedObjectsClientMock } from 'src/core/server/mocks'; import { createPackagePolicyMock } from '../../common/mocks'; import { packagePolicyService } from './package_policy'; import { PackageInfo, PackagePolicySOAttributes } from '../types'; @@ -345,9 +345,11 @@ describe('Package policy service', () => { throw savedObjectsClient.errors.createConflictError('abc', '123'); } ); + const elasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; await expect( packagePolicyService.update( savedObjectsClient, + elasticsearchClient, 'the-package-policy-id', createPackagePolicyMock() ) diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 95b1a43ec2e5..605b0f6cf65c 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -3,7 +3,12 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { KibanaRequest, RequestHandlerContext, SavedObjectsClientContract } from 'src/core/server'; +import { + ElasticsearchClient, + KibanaRequest, + RequestHandlerContext, + SavedObjectsClientContract, +} from 'src/core/server'; import uuid from 'uuid'; import { AuthenticatedUser } from '../../../security/server'; import { @@ -47,6 +52,7 @@ function getDataset(st: string) { class PackagePolicyService { public async create( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, callCluster: CallESAsCurrentUser, packagePolicy: NewPackagePolicy, options?: { id?: string; user?: AuthenticatedUser; bumpRevision?: boolean } @@ -116,10 +122,16 @@ class PackagePolicyService { ); // Assign it to the given agent policy - await agentPolicyService.assignPackagePolicies(soClient, packagePolicy.policy_id, [newSo.id], { - user: options?.user, - bumpRevision: options?.bumpRevision ?? true, - }); + await agentPolicyService.assignPackagePolicies( + soClient, + esClient, + packagePolicy.policy_id, + [newSo.id], + { + user: options?.user, + bumpRevision: options?.bumpRevision ?? true, + } + ); return { id: newSo.id, @@ -130,6 +142,7 @@ class PackagePolicyService { public async bulkCreate( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, packagePolicies: NewPackagePolicy[], agentPolicyId: string, options?: { user?: AuthenticatedUser; bumpRevision?: boolean } @@ -167,6 +180,7 @@ class PackagePolicyService { // Assign it to the given agent policy await agentPolicyService.assignPackagePolicies( soClient, + esClient, agentPolicyId, newSos.map((newSo) => newSo.id), { @@ -252,6 +266,7 @@ class PackagePolicyService { public async update( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, id: string, packagePolicy: UpdatePackagePolicy, options?: { user?: AuthenticatedUser } @@ -308,7 +323,7 @@ class PackagePolicyService { ); // Bump revision of associated agent policy - await agentPolicyService.bumpRevision(soClient, packagePolicy.policy_id, { + await agentPolicyService.bumpRevision(soClient, esClient, packagePolicy.policy_id, { user: options?.user, }); @@ -317,6 +332,7 @@ class PackagePolicyService { public async delete( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, ids: string[], options?: { user?: AuthenticatedUser; skipUnassignFromAgentPolicies?: boolean } ): Promise { @@ -331,6 +347,7 @@ class PackagePolicyService { if (!options?.skipUnassignFromAgentPolicies) { await agentPolicyService.unassignPackagePolicies( soClient, + esClient, packagePolicy.policy_id, [packagePolicy.id], { diff --git a/x-pack/plugins/fleet/server/services/setup.test.ts b/x-pack/plugins/fleet/server/services/setup.test.ts index bb01862aaf31..1e2440ba3b54 100644 --- a/x-pack/plugins/fleet/server/services/setup.test.ts +++ b/x-pack/plugins/fleet/server/services/setup.test.ts @@ -41,8 +41,9 @@ describe('setupIngestManager', () => { soClient.find = mockedMethodThrowsError(); soClient.get = mockedMethodThrowsError(); soClient.update = mockedMethodThrowsError(); + const esClient = context.core.elasticsearch.client.asCurrentUser; - const setupPromise = setupIngestManager(soClient, jest.fn()); + const setupPromise = setupIngestManager(soClient, esClient, jest.fn()); await expect(setupPromise).rejects.toThrow('SO method mocked to throw'); await expect(setupPromise).rejects.toThrow(Error); }); @@ -53,8 +54,9 @@ describe('setupIngestManager', () => { soClient.find = mockedMethodThrowsCustom(); soClient.get = mockedMethodThrowsCustom(); soClient.update = mockedMethodThrowsCustom(); + const esClient = context.core.elasticsearch.client.asCurrentUser; - const setupPromise = setupIngestManager(soClient, jest.fn()); + const setupPromise = setupIngestManager(soClient, esClient, jest.fn()); await expect(setupPromise).rejects.toThrow('method mocked to throw'); await expect(setupPromise).rejects.toThrow(CustomTestError); }); diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts index c37eed191088..1ce7b1d85c8e 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'; @@ -39,13 +39,15 @@ export interface SetupStatus { export async function setupIngestManager( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, callCluster: CallESAsCurrentUser ): Promise { - return awaitIfPending(async () => createSetupSideEffects(soClient, callCluster)); + return awaitIfPending(async () => createSetupSideEffects(soClient, esClient, callCluster)); } async function createSetupSideEffects( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, callCluster: CallESAsCurrentUser ): Promise { const [ @@ -56,7 +58,7 @@ async function createSetupSideEffects( // packages installed by default ensureInstalledDefaultPackages(soClient, callCluster), outputService.ensureDefaultOutput(soClient), - agentPolicyService.ensureDefaultAgentPolicy(soClient), + agentPolicyService.ensureDefaultAgentPolicy(soClient, esClient), settingsService.getSettings(soClient).catch((e: any) => { if (e.isBoom && e.output.statusCode === 404) { const defaultSettings = createDefaultSettings(); @@ -109,6 +111,7 @@ async function createSetupSideEffects( if (!isInstalled) { await addPackageToAgentPolicy( soClient, + esClient, callCluster, installedPackage, agentPolicyWithPackagePolicies, @@ -125,6 +128,7 @@ async function createSetupSideEffects( export async function setupFleet( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, callCluster: CallESAsCurrentUser, options?: { forceRecreate?: boolean } ) { @@ -189,7 +193,7 @@ export async function setupFleet( await Promise.all( agentPolicies.map((agentPolicy) => { - return generateEnrollmentAPIKey(soClient, { + return generateEnrollmentAPIKey(soClient, esClient, { name: `Default`, agentPolicyId: agentPolicy.id, }); @@ -209,6 +213,7 @@ function generateRandomPassword() { async function addPackageToAgentPolicy( soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, callCluster: CallESAsCurrentUser, packageToInstall: Installation, agentPolicy: AgentPolicy, @@ -227,7 +232,7 @@ async function addPackageToAgentPolicy( agentPolicy.namespace ); - await packagePolicyService.create(soClient, callCluster, newPackagePolicy, { + await packagePolicyService.create(soClient, esClient, callCluster, newPackagePolicy, { bumpRevision: false, }); } diff --git a/x-pack/plugins/fleet/server/types/index.tsx b/x-pack/plugins/fleet/server/types/index.tsx index 7e6e6d5e408b..d3ac40215917 100644 --- a/x-pack/plugins/fleet/server/types/index.tsx +++ b/x-pack/plugins/fleet/server/types/index.tsx @@ -77,6 +77,8 @@ export { PostAgentCheckinRequest, DataType, dataTypes, + // Fleet Server types + FleetServerEnrollmentAPIKey, } from '../../common'; export type CallESAsCurrentUser = LegacyScopedClusterClient['callAsCurrentUser']; diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts index 5773b88fa2be..225592fa8e68 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts @@ -5,7 +5,11 @@ */ import { Subject } from 'rxjs'; -import { loggingSystemMock, savedObjectsServiceMock } from 'src/core/server/mocks'; +import { + elasticsearchServiceMock, + loggingSystemMock, + savedObjectsServiceMock, +} from 'src/core/server/mocks'; import { LicenseService } from '../../../../common/license/license'; import { createPackagePolicyServiceMock } from '../../../../../fleet/server/mocks'; import { PolicyWatcher } from './license_watch'; @@ -31,6 +35,7 @@ const MockPPWithEndpointPolicy = (cb?: (p: PolicyConfig) => PolicyConfig): Packa describe('Policy-Changing license watcher', () => { const logger = loggingSystemMock.create().get('license_watch.test'); const soStartMock = savedObjectsServiceMock.createStartContract(); + const esStartMock = elasticsearchServiceMock.createStart(); let packagePolicySvcMock: jest.Mocked; const Platinum = licenseMock.createLicense({ license: { type: 'platinum', mode: 'platinum' } }); @@ -45,7 +50,7 @@ describe('Policy-Changing license watcher', () => { // mock a license-changing service to test reactivity const licenseEmitter: Subject = new Subject(); const licenseService = new LicenseService(); - const pw = new PolicyWatcher(packagePolicySvcMock, soStartMock, logger); + const pw = new PolicyWatcher(packagePolicySvcMock, soStartMock, esStartMock, logger); // swap out watch function, just to ensure it gets called when a license change happens const mockWatch = jest.fn(); @@ -90,7 +95,7 @@ describe('Policy-Changing license watcher', () => { perPage: 100, }); - const pw = new PolicyWatcher(packagePolicySvcMock, soStartMock, logger); + const pw = new PolicyWatcher(packagePolicySvcMock, soStartMock, esStartMock, logger); await pw.watch(Gold); // just manually trigger with a given license expect(packagePolicySvcMock.list.mock.calls.length).toBe(3); // should have asked for 3 pages of resuts @@ -119,14 +124,14 @@ describe('Policy-Changing license watcher', () => { perPage: 100, }); - const pw = new PolicyWatcher(packagePolicySvcMock, soStartMock, logger); + const pw = new PolicyWatcher(packagePolicySvcMock, soStartMock, esStartMock, logger); // emulate a license change below paid tier await pw.watch(Basic); expect(packagePolicySvcMock.update).toHaveBeenCalled(); expect( - packagePolicySvcMock.update.mock.calls[0][2].inputs[0].config!.policy.value.windows.popup + packagePolicySvcMock.update.mock.calls[0][3].inputs[0].config!.policy.value.windows.popup .malware.message ).not.toEqual(CustomMessage); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts b/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts index 2f0c3bf8fd5b..a8aa0f25b078 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts @@ -7,6 +7,8 @@ import { Subscription } from 'rxjs'; import { + ElasticsearchClient, + ElasticsearchServiceStart, KibanaRequest, Logger, SavedObjectsClientContract, @@ -28,15 +30,18 @@ import { isAtLeast, LicenseService } from '../../../../common/license/license'; export class PolicyWatcher { private logger: Logger; private soClient: SavedObjectsClientContract; + private esClient: ElasticsearchClient; private policyService: PackagePolicyServiceInterface; private subscription: Subscription | undefined; constructor( policyService: PackagePolicyServiceInterface, soStart: SavedObjectsServiceStart, + esStart: ElasticsearchServiceStart, logger: Logger ) { this.policyService = policyService; this.soClient = this.makeInternalSOClient(soStart); + this.esClient = esStart.client.asInternalUser; this.logger = logger; } @@ -113,11 +118,16 @@ export class PolicyWatcher { license ); try { - await this.policyService.update(this.soClient, policy.id, updatePolicy); + await this.policyService.update(this.soClient, this.esClient, policy.id, updatePolicy); } catch (e) { // try again for transient issues try { - await this.policyService.update(this.soClient, policy.id, updatePolicy); + await this.policyService.update( + this.soClient, + this.esClient, + policy.id, + updatePolicy + ); } catch (ee) { this.logger.warn( `Unable to remove platinum features from policy ${policy.id}: ${ee.message}` diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts index a79175b178c3..83732170fb5c 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts @@ -68,13 +68,15 @@ export const getMetadataListRequestHandler = function ( const unenrolledAgentIds = await findAllUnenrolledAgentIds( agentService, - context.core.savedObjects.client + context.core.savedObjects.client, + context.core.elasticsearch.client.asCurrentUser ); const statusIDs = request?.body?.filters?.host_status?.length ? await findAgentIDsByStatus( agentService, context.core.savedObjects.client, + context.core.elasticsearch.client.asCurrentUser, request.body?.filters?.host_status ) : undefined; @@ -193,6 +195,7 @@ async function findAgent( ?.getAgentService() ?.getAgent( metadataRequestContext.requestHandlerContext.core.savedObjects.client, + metadataRequestContext.requestHandlerContext.core.elasticsearch.client.asCurrentUser, hostMetadata.elastic.agent.id ); } catch (e) { @@ -267,6 +270,7 @@ export async function enrichHostMetadata( ?.getAgentService() ?.getAgentStatusById( metadataRequestContext.requestHandlerContext.core.savedObjects.client, + metadataRequestContext.requestHandlerContext.core.elasticsearch.client.asCurrentUser, elasticAgentId ); hostStatus = HOST_STATUS_MAPPING.get(status!) || HostStatus.ERROR; @@ -289,6 +293,7 @@ export async function enrichHostMetadata( ?.getAgentService() ?.getAgent( metadataRequestContext.requestHandlerContext.core.savedObjects.client, + metadataRequestContext.requestHandlerContext.core.elasticsearch.client.asCurrentUser, elasticAgentId ); const agentPolicy = await metadataRequestContext.endpointAppContextService diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.test.ts index e9a1f1e24fa5..d7fe8b75cbc8 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.test.ts @@ -4,9 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from 'kibana/server'; +import { ElasticsearchClient, SavedObjectsClientContract } from 'kibana/server'; import { findAgentIDsByStatus } from './agent_status'; -import { savedObjectsClientMock } from '../../../../../../../../src/core/server/mocks'; +import { + elasticsearchServiceMock, + savedObjectsClientMock, +} from '../../../../../../../../src/core/server/mocks'; import { AgentService } from '../../../../../../fleet/server/services'; import { createMockAgentService } from '../../../../../../fleet/server/mocks'; import { Agent } from '../../../../../../fleet/common/types/models'; @@ -14,9 +17,11 @@ import { AgentStatusKueryHelper } from '../../../../../../fleet/common/services' describe('test filtering endpoint hosts by agent status', () => { let mockSavedObjectClient: jest.Mocked; + let mockElasticsearchClient: jest.Mocked; let mockAgentService: jest.Mocked; beforeEach(() => { mockSavedObjectClient = savedObjectsClientMock.create(); + mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; mockAgentService = createMockAgentService(); }); @@ -30,7 +35,12 @@ describe('test filtering endpoint hosts by agent status', () => { }) ); - const result = await findAgentIDsByStatus(mockAgentService, mockSavedObjectClient, ['online']); + const result = await findAgentIDsByStatus( + mockAgentService, + mockSavedObjectClient, + mockElasticsearchClient, + ['online'] + ); expect(result).toBeDefined(); }); @@ -53,9 +63,14 @@ describe('test filtering endpoint hosts by agent status', () => { }) ); - const result = await findAgentIDsByStatus(mockAgentService, mockSavedObjectClient, ['offline']); + const result = await findAgentIDsByStatus( + mockAgentService, + mockSavedObjectClient, + mockElasticsearchClient, + ['offline'] + ); const offlineKuery = AgentStatusKueryHelper.buildKueryForOfflineAgents(); - expect(mockAgentService.listAgents.mock.calls[0][1].kuery).toEqual( + expect(mockAgentService.listAgents.mock.calls[0][2].kuery).toEqual( expect.stringContaining(offlineKuery) ); expect(result).toBeDefined(); @@ -81,13 +96,15 @@ describe('test filtering endpoint hosts by agent status', () => { }) ); - const result = await findAgentIDsByStatus(mockAgentService, mockSavedObjectClient, [ - 'unenrolling', - 'error', - ]); + const result = await findAgentIDsByStatus( + mockAgentService, + mockSavedObjectClient, + mockElasticsearchClient, + ['unenrolling', 'error'] + ); const unenrollKuery = AgentStatusKueryHelper.buildKueryForUnenrollingAgents(); const errorKuery = AgentStatusKueryHelper.buildKueryForErrorAgents(); - expect(mockAgentService.listAgents.mock.calls[0][1].kuery).toEqual( + expect(mockAgentService.listAgents.mock.calls[0][2].kuery).toEqual( expect.stringContaining(`${unenrollKuery} OR ${errorKuery}`) ); expect(result).toBeDefined(); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.ts index 395b05c0887e..4d3fd806dc63 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/agent_status.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from 'kibana/server'; +import { ElasticsearchClient, SavedObjectsClientContract } from 'kibana/server'; import { AgentService } from '../../../../../../fleet/server'; import { AgentStatusKueryHelper } from '../../../../../../fleet/common/services'; import { Agent } from '../../../../../../fleet/common/types/models'; @@ -20,6 +20,7 @@ const STATUS_QUERY_MAP = new Map([ export async function findAgentIDsByStatus( agentService: AgentService, soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, status: string[], pageSize: number = 1000 ): Promise { @@ -39,7 +40,7 @@ export async function findAgentIDsByStatus( let hasMore = true; while (hasMore) { - const agents = await agentService.listAgents(soClient, searchOptions(page++)); + const agents = await agentService.listAgents(soClient, esClient, searchOptions(page++)); result.push(...agents.agents.map((agent: Agent) => agent.id)); hasMore = agents.agents.length > 0; } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.test.ts index c88f11422d0f..ea68f6270e73 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.test.ts @@ -4,18 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from 'kibana/server'; +import { ElasticsearchClient, SavedObjectsClientContract } from 'kibana/server'; import { findAllUnenrolledAgentIds } from './unenroll'; -import { savedObjectsClientMock } from '../../../../../../../../src/core/server/mocks'; +import { + elasticsearchServiceMock, + savedObjectsClientMock, +} from '../../../../../../../../src/core/server/mocks'; import { AgentService } from '../../../../../../fleet/server/services'; import { createMockAgentService } from '../../../../../../fleet/server/mocks'; import { Agent } from '../../../../../../fleet/common/types/models'; describe('test find all unenrolled Agent id', () => { let mockSavedObjectClient: jest.Mocked; + let mockElasticsearchClient: jest.Mocked; let mockAgentService: jest.Mocked; beforeEach(() => { mockSavedObjectClient = savedObjectsClientMock.create(); + mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; mockAgentService = createMockAgentService(); }); @@ -53,7 +58,11 @@ describe('test find all unenrolled Agent id', () => { perPage: 1, }) ); - const agentIds = await findAllUnenrolledAgentIds(mockAgentService, mockSavedObjectClient); + const agentIds = await findAllUnenrolledAgentIds( + mockAgentService, + mockSavedObjectClient, + mockElasticsearchClient + ); expect(agentIds).toBeTruthy(); expect(agentIds).toEqual(['id1', 'id2']); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.ts index 1abea86c1a49..45664f087f1b 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/support/unenroll.ts @@ -4,13 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from 'kibana/server'; +import { ElasticsearchClient, SavedObjectsClientContract } from 'kibana/server'; import { AgentService } from '../../../../../../fleet/server'; import { Agent } from '../../../../../../fleet/common/types/models'; export async function findAllUnenrolledAgentIds( agentService: AgentService, soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, pageSize: number = 1000 ): Promise { const searchOptions = (pageNum: number) => { @@ -29,7 +30,11 @@ export async function findAllUnenrolledAgentIds( let hasMore = true; while (hasMore) { - const unenrolledAgents = await agentService.listAgents(soClient, searchOptions(page++)); + const unenrolledAgents = await agentService.listAgents( + soClient, + esClient, + searchOptions(page++) + ); result.push(...unenrolledAgents.agents.map((agent: Agent) => agent.id)); hasMore = unenrolledAgents.agents.length > 0; } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.ts index 728e3279c52a..46b67706c99a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.ts @@ -44,6 +44,7 @@ export const getAgentPolicySummaryHandler = function ( const result = await getAgentPolicySummary( endpointAppContext, context.core.savedObjects.client, + context.core.elasticsearch.client.asCurrentUser, request.query.package_name, request.query?.policy_id || undefined ); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.ts b/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.ts index dd4ade1906bc..f52535053a53 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/policy/service.ts @@ -5,7 +5,11 @@ */ import { SearchResponse } from 'elasticsearch'; -import { ILegacyScopedClusterClient, SavedObjectsClientContract } from 'kibana/server'; +import { + ElasticsearchClient, + ILegacyScopedClusterClient, + SavedObjectsClientContract, +} from 'kibana/server'; import { GetHostPolicyResponse, HostPolicyResponse } from '../../../../common/endpoint/types'; import { INITIAL_POLICY_ID } from './index'; import { Agent } from '../../../../../fleet/common/types/models'; @@ -73,6 +77,7 @@ const transformAgentVersionMap = (versionMap: Map): { [key: stri export async function getAgentPolicySummary( endpointAppContext: EndpointAppContext, soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, packageName: string, policyId?: string, pageSize: number = 1000 @@ -83,6 +88,7 @@ export async function getAgentPolicySummary( await agentVersionsMap( endpointAppContext, soClient, + esClient, `${agentQuery} AND ${AGENT_SAVED_OBJECT_TYPE}.policy_id:${policyId}`, pageSize ) @@ -90,13 +96,14 @@ export async function getAgentPolicySummary( } return transformAgentVersionMap( - await agentVersionsMap(endpointAppContext, soClient, agentQuery, pageSize) + await agentVersionsMap(endpointAppContext, soClient, esClient, agentQuery, pageSize) ); } export async function agentVersionsMap( endpointAppContext: EndpointAppContext, soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, kqlQuery: string, pageSize: number = 1000 ): Promise> { @@ -115,7 +122,7 @@ export async function agentVersionsMap( while (hasMore) { const queryResult = await endpointAppContext.service .getAgentService()! - .listAgents(soClient, searchOptions(page++)); + .listAgents(soClient, esClient, searchOptions(page++)); queryResult.agents.forEach((agent: Agent) => { const agentVersion = agent.local_metadata?.elastic?.agent?.version; if (result.has(agentVersion)) { diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts index 9cb3f3d20543..2bd4fcf348f7 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts @@ -191,7 +191,7 @@ describe('manifest_manager', () => { expect(packagePolicyService.update.mock.calls.length).toEqual(2); expect( - packagePolicyService.update.mock.calls[0][2].inputs[0].config!.artifact_manifest.value + packagePolicyService.update.mock.calls[0][3].inputs[0].config!.artifact_manifest.value ).toEqual({ manifest_version: '1.0.1', schema_version: 'v1', diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts index 9f45f39a392f..44a0fb15e48d 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts @@ -293,7 +293,13 @@ export class ManifestManager { }; try { - await this.packagePolicyService.update(this.savedObjectsClient, id, newPackagePolicy); + await this.packagePolicyService.update( + this.savedObjectsClient, + // @ts-ignore + undefined, + id, + newPackagePolicy + ); this.logger.debug( `Updated package policy ${id} with manifest version ${manifest.getSemanticVersion()}` ); diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index d51346ee9645..020237ad497f 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -355,6 +355,7 @@ export class Plugin implements IPlugin