diff --git a/x-pack/plugins/fleet/common/constants/plugin.ts b/x-pack/plugins/fleet/common/constants/plugin.ts index ee0e1e5caeae4..86211ba3727eb 100644 --- a/x-pack/plugins/fleet/common/constants/plugin.ts +++ b/x-pack/plugins/fleet/common/constants/plugin.ts @@ -5,5 +5,5 @@ * 2.0. */ -export const PLUGIN_ID = 'fleet'; -export const INTEGRATIONS_PLUGIN_ID = 'integrations'; +export const PLUGIN_ID = 'fleet' as const; +export const INTEGRATIONS_PLUGIN_ID = 'integrations' as const; diff --git a/x-pack/plugins/fleet/server/errors/index.ts b/x-pack/plugins/fleet/server/errors/index.ts index b2039dad4d57c..c72d476e4c466 100644 --- a/x-pack/plugins/fleet/server/errors/index.ts +++ b/x-pack/plugins/fleet/server/errors/index.ts @@ -52,6 +52,7 @@ export class HostedAgentPolicyRestrictionRelatedError extends IngestManagerError export class FleetSetupError extends IngestManagerError {} export class GenerateServiceTokenError extends IngestManagerError {} +export class FleetUnauthorizedError extends IngestManagerError {} export class OutputUnauthorizedError extends IngestManagerError {} diff --git a/x-pack/plugins/fleet/server/index.ts b/x-pack/plugins/fleet/server/index.ts index 17425050a4e10..9fc0edd0b7cf8 100644 --- a/x-pack/plugins/fleet/server/index.ts +++ b/x-pack/plugins/fleet/server/index.ts @@ -19,6 +19,7 @@ import { FleetPlugin } from './plugin'; export type { AgentService, + AgentClient, ESIndexPatternService, PackageService, AgentPolicyServiceInterface, @@ -34,8 +35,9 @@ export type { PutPackagePolicyUpdateCallback, PostPackagePolicyDeleteCallback, PostPackagePolicyCreateCallback, + FleetRequestHandlerContext, } from './types'; -export { AgentNotFoundError } from './errors'; +export { AgentNotFoundError, FleetUnauthorizedError } from './errors'; export const config: PluginConfigDescriptor = { exposeToBrowser: { diff --git a/x-pack/plugins/fleet/server/mocks/index.ts b/x-pack/plugins/fleet/server/mocks/index.ts index bd7f192dc7fd2..90a0addfae490 100644 --- a/x-pack/plugins/fleet/server/mocks/index.ts +++ b/x-pack/plugins/fleet/server/mocks/index.ts @@ -11,16 +11,19 @@ import { loggingSystemMock, savedObjectsServiceMock, coreMock, + savedObjectsClientMock, } from '../../../../../src/core/server/mocks'; import { dataPluginMock } from '../../../../../src/plugins/data/server/mocks'; import { licensingMock } from '../../../../plugins/licensing/server/mocks'; import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks'; import { securityMock } from '../../../security/server/mocks'; import type { PackagePolicyServiceInterface } from '../services/package_policy'; -import type { AgentPolicyServiceInterface, AgentService } from '../services'; +import type { AgentPolicyServiceInterface, PackageService } from '../services'; import type { FleetAppContext } from '../plugin'; import { createMockTelemetryEventsSender } from '../telemetry/__mocks__'; import type { FleetAuthz } from '../../common'; +import { agentServiceMock } from '../services/agents/agent_service.mock'; +import type { FleetRequestHandlerContext } from '../types'; // Export all mocks from artifacts export * from '../services/artifacts/mocks'; @@ -65,10 +68,26 @@ export const createAppContextStartContractMock = (): MockedFleetAppContext => { }; }; +export const createFleetRequestHandlerContextMock = (): jest.Mocked< + FleetRequestHandlerContext['fleet'] +> => { + return { + authz: createFleetAuthzMock(), + agentClient: { + asCurrentUser: agentServiceMock.createClient(), + asInternalUser: agentServiceMock.createClient(), + }, + epm: { + internalSoClient: savedObjectsClientMock.create(), + }, + }; +}; + function createCoreRequestHandlerContextMock() { return { core: coreMock.createRequestHandlerContext(), licensing: licensingMock.createRequestHandlerContext(), + fleet: createFleetRequestHandlerContextMock(), }; } @@ -113,33 +132,40 @@ export const createMockAgentPolicyService = (): jest.Mocked => { +export const createMockAgentService = () => agentServiceMock.create(); + +/** + * Creates a mock AgentClient + */ +export const createMockAgentClient = () => agentServiceMock.createClient(); + +export const createMockPackageService = (): PackageService => { return { - getAgentStatusById: jest.fn(), - getAgentStatusForAgentPolicy: jest.fn(), - getAgent: jest.fn(), - listAgents: jest.fn(), + getInstallation: jest.fn(), + ensureInstalledPackage: jest.fn(), }; }; /** * Creates mock `authz` object */ -export const fleetAuthzMock: FleetAuthz = { - fleet: { - all: true, - setup: true, - readEnrollmentTokens: true, - }, - integrations: { - readPackageInfo: true, - readInstalledPackages: true, - installPackages: true, - upgradePackages: true, - removePackages: true, - readPackageSettings: true, - writePackageSettings: true, - readIntegrationPolicies: true, - writeIntegrationPolicies: true, - }, +export const createFleetAuthzMock = (): FleetAuthz => { + return { + fleet: { + all: true, + setup: true, + readEnrollmentTokens: true, + }, + integrations: { + readPackageInfo: true, + readInstalledPackages: true, + installPackages: true, + upgradePackages: true, + removePackages: true, + readPackageSettings: true, + writePackageSettings: true, + readIntegrationPolicies: true, + writeIntegrationPolicies: true, + }, + }; }; diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 3ee83a91e0df5..72955923f7d3e 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -16,6 +16,7 @@ import type { SavedObjectsServiceStart, HttpServiceSetup, KibanaRequest, + ElasticsearchClient, } from 'kibana/server'; import type { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; @@ -71,13 +72,8 @@ import { ESIndexPatternSavedObjectService, agentPolicyService, packagePolicyService, + AgentServiceImpl, } from './services'; -import { - getAgentStatusById, - getAgentStatusForAgentPolicy, - getAgentsByKuery, - getAgentById, -} from './services/agents'; import { registerFleetUsageCollector } from './collectors/register'; import { getInstallation, ensureInstalledPackage } from './services/epm/packages'; import { getAuthzFromRequest, RouterWrappers } from './routes/security'; @@ -184,6 +180,8 @@ export class FleetPlugin private encryptedSavedObjectsSetup?: EncryptedSavedObjectsPluginSetup; private readonly telemetryEventsSender: TelemetryEventsSender; + private agentService?: AgentService; + constructor(private readonly initializerContext: PluginInitializerContext) { this.config$ = this.initializerContext.config.create(); this.isProductionMode = this.initializerContext.env.mode.prod; @@ -209,7 +207,7 @@ export class FleetPlugin // TODO: Flesh out privileges if (deps.features) { deps.features.registerKibanaFeature({ - id: 'fleet', + id: PLUGIN_ID, name: 'Fleet and Integrations', category: DEFAULT_APP_CATEGORIES.management, app: [PLUGIN_ID, INTEGRATIONS_PLUGIN_ID, 'kibana'], @@ -234,7 +232,7 @@ export class FleetPlugin }, privileges: { all: { - api: [`fleet-read`, `fleet-all`, `integrations-all`, `integrations-read`], + api: [`${PLUGIN_ID}-read`, `${PLUGIN_ID}-all`, `integrations-all`, `integrations-read`], app: [PLUGIN_ID, INTEGRATIONS_PLUGIN_ID, 'kibana'], catalogue: ['fleet'], savedObject: { @@ -244,7 +242,7 @@ export class FleetPlugin ui: ['show', 'read', 'write'], }, read: { - api: [`fleet-read`, `integrations-read`], + api: [`${PLUGIN_ID}-read`, `integrations-read`], app: [PLUGIN_ID, INTEGRATIONS_PLUGIN_ID, 'kibana'], catalogue: ['fleet'], // TODO: check if this is actually available to read user savedObject: { @@ -257,19 +255,33 @@ export class FleetPlugin }); } - core.http.registerRouteHandlerContext( - 'fleet', - async (coreContext, request) => ({ - authz: await getAuthzFromRequest(request), - epm: { - // Use a lazy getter to avoid constructing this client when not used by a request handler - get internalSoClient() { - return appContextService - .getSavedObjects() - .getScopedClient(request, { excludedWrappers: ['security'] }); + core.http.registerRouteHandlerContext( + PLUGIN_ID, + async (context, request) => { + const plugin = this; + + return { + get agentClient() { + const agentService = plugin.setupAgentService( + context.core.elasticsearch.client.asInternalUser + ); + + return { + asCurrentUser: agentService.asScoped(request), + asInternalUser: agentService.asInternalUser, + }; }, - }, - }) + authz: await getAuthzFromRequest(request), + epm: { + // Use a lazy getter to avoid constructing this client when not used by a request handler + get internalSoClient() { + return appContextService + .getSavedObjects() + .getScopedClient(request, { excludedWrappers: ['security'] }); + }, + }, + }; + } ); const router: FleetRouter = core.http.createRouter(); @@ -362,12 +374,7 @@ export class FleetPlugin getInstallation, ensureInstalledPackage, }, - agentService: { - getAgent: getAgentById, - listAgents: getAgentsByKuery, - getAgentStatusById, - getAgentStatusForAgentPolicy, - }, + agentService: this.setupAgentService(core.elasticsearch.client.asInternalUser), agentPolicyService: { get: agentPolicyService.get, list: agentPolicyService.list, @@ -390,4 +397,13 @@ export class FleetPlugin licenseService.stop(); this.telemetryEventsSender.stop(); } + + private setupAgentService(internalEsClient: ElasticsearchClient): AgentService { + if (this.agentService) { + return this.agentService; + } + + this.agentService = new AgentServiceImpl(internalEsClient); + return this.agentService; + } } diff --git a/x-pack/plugins/fleet/server/routes/security.ts b/x-pack/plugins/fleet/server/routes/security.ts index 0b7065edf63ba..8e037c25ceca9 100644 --- a/x-pack/plugins/fleet/server/routes/security.ts +++ b/x-pack/plugins/fleet/server/routes/security.ts @@ -24,7 +24,7 @@ function checkSecurityEnabled() { return appContextService.hasSecurity() && appContextService.getSecurityLicense().isEnabled(); } -function checkSuperuser(req: KibanaRequest) { +export function checkSuperuser(req: KibanaRequest) { if (!checkSecurityEnabled()) { return false; } diff --git a/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts b/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts index 4f034a0add32f..d48d80add2435 100644 --- a/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts +++ b/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts @@ -9,7 +9,8 @@ import { httpServerMock, savedObjectsClientMock } from 'src/core/server/mocks'; import type { PostFleetSetupResponse } from '../../../common'; import { RegistryError } from '../../errors'; -import { createAppContextStartContractMock, xpackMocks, fleetAuthzMock } from '../../mocks'; +import { createAppContextStartContractMock, xpackMocks, createFleetAuthzMock } from '../../mocks'; +import { agentServiceMock } from '../../services/agents/agent_service.mock'; import { appContextService } from '../../services/app_context'; import { setupFleet } from '../../services/setup'; import type { FleetRequestHandlerContext } from '../../types'; @@ -34,7 +35,11 @@ describe('FleetSetupHandler', () => { context = { ...xpackMocks.createRequestHandlerContext(), fleet: { - authz: fleetAuthzMock, + agentClient: { + asCurrentUser: agentServiceMock.createClient(), + asInternalUser: agentServiceMock.createClient(), + }, + authz: createFleetAuthzMock(), epm: { internalSoClient: savedObjectsClientMock.create(), }, diff --git a/x-pack/plugins/fleet/server/services/agents/agent_service.mock.ts b/x-pack/plugins/fleet/server/services/agents/agent_service.mock.ts new file mode 100644 index 0000000000000..d1d45ad48f30e --- /dev/null +++ b/x-pack/plugins/fleet/server/services/agents/agent_service.mock.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { AgentClient, AgentService } from './agent_service'; + +const createClientMock = (): jest.Mocked => ({ + getAgent: jest.fn(), + getAgentStatusById: jest.fn(), + getAgentStatusForAgentPolicy: jest.fn(), + listAgents: jest.fn(), +}); + +const createServiceMock = (): jest.Mocked => ({ + asInternalUser: createClientMock(), + asScoped: jest.fn().mockReturnValue(createClientMock()), +}); + +export const agentServiceMock = { + createClient: createClientMock, + create: createServiceMock, +}; diff --git a/x-pack/plugins/fleet/server/services/agents/agent_service.test.ts b/x-pack/plugins/fleet/server/services/agents/agent_service.test.ts new file mode 100644 index 0000000000000..7dd61be1ab1be --- /dev/null +++ b/x-pack/plugins/fleet/server/services/agents/agent_service.test.ts @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +jest.mock('../../routes/security'); +jest.mock('./crud'); +jest.mock('./status'); + +import type { ElasticsearchClient } from '../../../../../../src/core/server'; +import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; +import { FleetUnauthorizedError } from '../../errors'; + +import { checkSuperuser } from '../../routes/security'; + +import type { AgentClient } from './agent_service'; +import { AgentServiceImpl } from './agent_service'; +import { getAgentsByKuery, getAgentById } from './crud'; +import { getAgentStatusById, getAgentStatusForAgentPolicy } from './status'; + +const mockCheckSuperuser = checkSuperuser as jest.Mock; +const mockGetAgentsByKuery = getAgentsByKuery as jest.Mock; +const mockGetAgentById = getAgentById as jest.Mock; +const mockGetAgentStatusById = getAgentStatusById as jest.Mock; +const mockGetAgentStatusForAgentPolicy = getAgentStatusForAgentPolicy as jest.Mock; + +describe('AgentService', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('asScoped', () => { + describe('without required privilege', () => { + const agentClient = new AgentServiceImpl( + elasticsearchServiceMock.createElasticsearchClient() + ).asScoped(httpServerMock.createKibanaRequest()); + + beforeEach(() => mockCheckSuperuser.mockReturnValue(false)); + + it('rejects on listAgents', async () => { + await expect(agentClient.listAgents({ showInactive: true })).rejects.toThrowError( + new FleetUnauthorizedError( + `User does not have adequate permissions to access Fleet agents.` + ) + ); + }); + + it('rejects on getAgent', async () => { + await expect(agentClient.getAgent('foo')).rejects.toThrowError( + new FleetUnauthorizedError( + `User does not have adequate permissions to access Fleet agents.` + ) + ); + }); + + it('rejects on getAgentStatusById', async () => { + await expect(agentClient.getAgentStatusById('foo')).rejects.toThrowError( + new FleetUnauthorizedError( + `User does not have adequate permissions to access Fleet agents.` + ) + ); + }); + + it('rejects on getAgentStatusForAgentPolicy', async () => { + await expect(agentClient.getAgentStatusForAgentPolicy()).rejects.toThrowError( + new FleetUnauthorizedError( + `User does not have adequate permissions to access Fleet agents.` + ) + ); + }); + }); + + describe('with required privilege', () => { + const mockEsClient = elasticsearchServiceMock.createElasticsearchClient(); + const agentClient = new AgentServiceImpl(mockEsClient).asScoped( + httpServerMock.createKibanaRequest() + ); + + beforeEach(() => mockCheckSuperuser.mockReturnValue(true)); + + expectApisToCallServicesSuccessfully(mockEsClient, agentClient); + }); + }); + + describe('asInternalUser', () => { + const mockEsClient = elasticsearchServiceMock.createElasticsearchClient(); + const agentClient = new AgentServiceImpl(mockEsClient).asInternalUser; + + expectApisToCallServicesSuccessfully(mockEsClient, agentClient); + }); +}); + +function expectApisToCallServicesSuccessfully( + mockEsClient: ElasticsearchClient, + agentClient: AgentClient +) { + test('client.listAgents calls getAgentsByKuery and returns results', async () => { + mockGetAgentsByKuery.mockResolvedValue('getAgentsByKuery success'); + await expect(agentClient.listAgents({ showInactive: true })).resolves.toEqual( + 'getAgentsByKuery success' + ); + expect(mockGetAgentsByKuery).toHaveBeenCalledWith(mockEsClient, { showInactive: true }); + }); + + test('client.getAgent calls getAgentById and returns results', async () => { + mockGetAgentById.mockResolvedValue('getAgentById success'); + await expect(agentClient.getAgent('foo-id')).resolves.toEqual('getAgentById success'); + expect(mockGetAgentById).toHaveBeenCalledWith(mockEsClient, 'foo-id'); + }); + + test('client.getAgentStatusById calls getAgentStatusById and returns results', async () => { + mockGetAgentStatusById.mockResolvedValue('getAgentStatusById success'); + await expect(agentClient.getAgentStatusById('foo-id')).resolves.toEqual( + 'getAgentStatusById success' + ); + expect(mockGetAgentStatusById).toHaveBeenCalledWith(mockEsClient, 'foo-id'); + }); + + test('client.getAgentStatusForAgentPolicy calls getAgentStatusForAgentPolicy and returns results', async () => { + mockGetAgentStatusForAgentPolicy.mockResolvedValue('getAgentStatusForAgentPolicy success'); + await expect(agentClient.getAgentStatusForAgentPolicy('foo-id', 'foo-filter')).resolves.toEqual( + 'getAgentStatusForAgentPolicy success' + ); + expect(mockGetAgentStatusForAgentPolicy).toHaveBeenCalledWith( + mockEsClient, + 'foo-id', + 'foo-filter' + ); + }); +} diff --git a/x-pack/plugins/fleet/server/services/agents/agent_service.ts b/x-pack/plugins/fleet/server/services/agents/agent_service.ts new file mode 100644 index 0000000000000..0286c29cba0c5 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/agents/agent_service.ts @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* eslint-disable max-classes-per-file */ + +import type { ElasticsearchClient, KibanaRequest } from 'kibana/server'; + +import type { AgentStatus, ListWithKuery } from '../../types'; +import type { Agent, GetAgentStatusResponse } from '../../../common'; + +import { checkSuperuser } from '../../routes/security'; + +import { FleetUnauthorizedError } from '../../errors'; + +import { getAgentsByKuery, getAgentById } from './crud'; +import { getAgentStatusById, getAgentStatusForAgentPolicy } from './status'; + +/** + * A service for interacting with Agent data. See {@link AgentClient} for more information. + * + * @public + */ +export interface AgentService { + /** + * Should be used for end-user requests to Kibana. APIs will return errors if user does not have appropriate access. + */ + asScoped(req: KibanaRequest): AgentClient; + + /** + * Only use for server-side usages (eg. telemetry), should not be used for end users unless an explicit authz check is + * done. + */ + asInternalUser: AgentClient; +} + +/** + * A client for interacting with data about an Agent + * + * @public + */ +export interface AgentClient { + /** + * Get an Agent by id + */ + getAgent(agentId: string): Promise; + + /** + * Return the status by the Agent's id + */ + getAgentStatusById(agentId: string): Promise; + + /** + * Return the status by the Agent's Policy id + */ + getAgentStatusForAgentPolicy( + agentPolicyId?: string, + filterKuery?: string + ): Promise; + + /** + * List agents + */ + listAgents( + options: ListWithKuery & { + showInactive: boolean; + } + ): Promise<{ + agents: Agent[]; + total: number; + page: number; + perPage: number; + }>; +} + +/** + * @internal + */ +class AgentClientImpl implements AgentClient { + constructor( + private readonly internalEsClient: ElasticsearchClient, + private readonly preflightCheck?: () => void | Promise + ) {} + + public async listAgents( + options: ListWithKuery & { + showInactive: boolean; + } + ) { + await this.#runPreflight(); + return getAgentsByKuery(this.internalEsClient, options); + } + + public async getAgent(agentId: string) { + await this.#runPreflight(); + return getAgentById(this.internalEsClient, agentId); + } + + public async getAgentStatusById(agentId: string) { + await this.#runPreflight(); + return getAgentStatusById(this.internalEsClient, agentId); + } + + public async getAgentStatusForAgentPolicy(agentPolicyId?: string, filterKuery?: string) { + await this.#runPreflight(); + return getAgentStatusForAgentPolicy(this.internalEsClient, agentPolicyId, filterKuery); + } + + #runPreflight = async () => { + if (this.preflightCheck) { + return this.preflightCheck(); + } + }; +} + +/** + * @internal + */ +export class AgentServiceImpl implements AgentService { + constructor(private readonly internalEsClient: ElasticsearchClient) {} + + public asScoped(req: KibanaRequest) { + const preflightCheck = () => { + if (!checkSuperuser(req)) { + throw new FleetUnauthorizedError( + `User does not have adequate permissions to access Fleet agents.` + ); + } + }; + + return new AgentClientImpl(this.internalEsClient, preflightCheck); + } + + public get asInternalUser() { + return new AgentClientImpl(this.internalEsClient); + } +} diff --git a/x-pack/plugins/fleet/server/services/agents/index.ts b/x-pack/plugins/fleet/server/services/agents/index.ts index 9b2846b68364e..7abe1873b70a3 100644 --- a/x-pack/plugins/fleet/server/services/agents/index.ts +++ b/x-pack/plugins/fleet/server/services/agents/index.ts @@ -13,3 +13,5 @@ export * from './update'; export * from './actions'; export * from './reassign'; export * from './setup'; +export { AgentServiceImpl } from './agent_service'; +export type { AgentClient, AgentService } from './agent_service'; diff --git a/x-pack/plugins/fleet/server/services/index.ts b/x-pack/plugins/fleet/server/services/index.ts index ab88e5af18efa..7e615f923b221 100644 --- a/x-pack/plugins/fleet/server/services/index.ts +++ b/x-pack/plugins/fleet/server/services/index.ts @@ -5,13 +5,8 @@ * 2.0. */ -import type { ElasticsearchClient, SavedObjectsClientContract } from 'kibana/server'; +import type { SavedObjectsClientContract } from 'kibana/server'; -import type { AgentStatus } from '../types'; - -import type { GetAgentStatusResponse } from '../../common'; - -import type { getAgentById, getAgentsByKuery } from './agents'; import type { agentPolicyService } from './agent_policy'; import * as settingsService from './settings'; import type { getInstallation, ensureInstalledPackage } from './epm/packages'; @@ -39,32 +34,6 @@ export interface PackageService { ensureInstalledPackage: typeof ensureInstalledPackage; } -/** - * A service that provides exported functions that return information about an Agent - */ -export interface AgentService { - /** - * Get an Agent by id - */ - getAgent: typeof getAgentById; - /** - * Return the status by the Agent's id - */ - getAgentStatusById(esClient: ElasticsearchClient, agentId: string): Promise; - /** - * Return the status by the Agent's Policy id - */ - getAgentStatusForAgentPolicy( - esClient: ElasticsearchClient, - agentPolicyId?: string, - filterKuery?: string - ): Promise; - /** - * List agents - */ - listAgents: typeof getAgentsByKuery; -} - export interface AgentPolicyServiceInterface { get: typeof agentPolicyService['get']; list: typeof agentPolicyService['list']; @@ -73,6 +42,10 @@ export interface AgentPolicyServiceInterface { getByIds: typeof agentPolicyService['getByIDs']; } +// Agent services +export { AgentServiceImpl } from './agents'; +export type { AgentClient, AgentService } from './agents'; + // Saved object services export { agentPolicyService } from './agent_policy'; export { packagePolicyService } from './package_policy'; diff --git a/x-pack/plugins/fleet/server/types/request_context.ts b/x-pack/plugins/fleet/server/types/request_context.ts index 8de68c91f4ef3..aafa8765723d1 100644 --- a/x-pack/plugins/fleet/server/types/request_context.ts +++ b/x-pack/plugins/fleet/server/types/request_context.ts @@ -14,11 +14,18 @@ import type { IRouter, } from '../../../../../src/core/server'; import type { FleetAuthz } from '../../common/authz'; +import type { AgentClient } from '../services'; /** @internal */ export interface FleetRequestHandlerContext extends RequestHandlerContext { fleet: { + /** {@link FleetAuthz} */ authz: FleetAuthz; + /** {@link AgentClient} */ + agentClient: { + asCurrentUser: AgentClient; + asInternalUser: AgentClient; + }; epm: { /** * Saved Objects client configured to use kibana_system privileges instead of end-user privileges. Should only be diff --git a/x-pack/plugins/osquery/server/lib/parse_agent_groups.ts b/x-pack/plugins/osquery/server/lib/parse_agent_groups.ts index 8fe60f59f01d7..c2c86e7fd119d 100644 --- a/x-pack/plugins/osquery/server/lib/parse_agent_groups.ts +++ b/x-pack/plugins/osquery/server/lib/parse_agent_groups.ts @@ -6,7 +6,7 @@ */ import { uniq } from 'lodash'; -import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; +import type { KibanaRequest, SavedObjectsClientContract } from 'src/core/server'; import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../fleet/common'; import { OSQUERY_INTEGRATION_NAME } from '../../common'; import { OsqueryAppContext } from './osquery_app_context_services'; @@ -34,7 +34,7 @@ const aggregateResults = async ( }; export const parseAgentSelection = async ( - esClient: ElasticsearchClient, + request: KibanaRequest, soClient: SavedObjectsClientContract, context: OsqueryAppContext, agentSelection: AgentSelection @@ -42,7 +42,7 @@ export const parseAgentSelection = async ( const selectedAgents: Set = new Set(); const addAgent = selectedAgents.add.bind(selectedAgents); const { allAgentsSelected, platformsSelected, policiesSelected, agents } = agentSelection; - const agentService = context.service.getAgentService(); + const agentService = context.service.getAgentService()?.asScoped(request); const packagePolicyService = context.service.getPackagePolicyService(); const kueryFragments = []; @@ -59,7 +59,7 @@ export const parseAgentSelection = async ( if (allAgentsSelected) { const kuery = kueryFragments.join(' and '); const fetchedAgents = await aggregateResults(async (page, perPage) => { - const res = await agentService.listAgents(esClient, { + const res = await agentService.listAgents({ perPage, page, kuery, @@ -80,7 +80,7 @@ export const parseAgentSelection = async ( kueryFragments.push(`(${groupFragments.join(' or ')})`); const kuery = kueryFragments.join(' and '); const fetchedAgents = await aggregateResults(async (page, perPage) => { - const res = await agentService.listAgents(esClient, { + const res = await agentService.listAgents({ perPage, page, kuery, diff --git a/x-pack/plugins/osquery/server/routes/action/create_action_route.ts b/x-pack/plugins/osquery/server/routes/action/create_action_route.ts index cebdbe7b8fe86..0188f1432c22b 100644 --- a/x-pack/plugins/osquery/server/routes/action/create_action_route.ts +++ b/x-pack/plugins/osquery/server/routes/action/create_action_route.ts @@ -46,7 +46,7 @@ export const createActionRoute = (router: IRouter, osqueryContext: OsqueryAppCon const { agentSelection } = request.body as { agentSelection: AgentSelection }; const selectedAgents = await parseAgentSelection( - esClient, + request, soClient, osqueryContext, agentSelection diff --git a/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_policies.ts b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_policies.ts index 06641cc60e13d..90de01702868a 100644 --- a/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_policies.ts +++ b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_policies.ts @@ -30,7 +30,6 @@ export const getAgentPoliciesRoute = (router: IRouter, osqueryContext: OsqueryAp }, async (context, request, response) => { const soClient = context.core.savedObjects.client; - const esClient = context.core.elasticsearch.client.asInternalUser; const agentService = osqueryContext.service.getAgentService(); const agentPolicyService = osqueryContext.service.getAgentPolicyService(); const packagePolicyService = osqueryContext.service.getPackagePolicyService(); @@ -51,7 +50,8 @@ export const getAgentPoliciesRoute = (router: IRouter, osqueryContext: OsqueryAp agentPolicies, (agentPolicy: GetAgentPoliciesResponseItem) => agentService - ?.getAgentStatusForAgentPolicy(esClient, agentPolicy.id) + ?.asScoped(request) + .getAgentStatusForAgentPolicy(agentPolicy.id) .then(({ total: agentTotal }) => (agentPolicy.agents = agentTotal)), { concurrency: 10 } ); diff --git a/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_status_for_agent_policy.ts b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_status_for_agent_policy.ts index dea4402472958..1f4f12648a25b 100644 --- a/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_status_for_agent_policy.ts +++ b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_status_for_agent_policy.ts @@ -28,11 +28,10 @@ export const getAgentStatusForAgentPolicyRoute = ( options: { tags: [`access:${PLUGIN_ID}-read`] }, }, async (context, request, response) => { - const esClient = context.core.elasticsearch.client.asInternalUser; - const results = await osqueryContext.service .getAgentService() - ?.getAgentStatusForAgentPolicy(esClient, request.query.policyId, request.query.kuery); + ?.asScoped(request) + .getAgentStatusForAgentPolicy(request.query.policyId, request.query.kuery); if (!results) { return response.ok({ body: {} }); diff --git a/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agents.ts b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agents.ts index f129e95fd9508..b638f92f19aa9 100644 --- a/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agents.ts +++ b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agents.ts @@ -20,14 +20,13 @@ export const getAgentsRoute = (router: IRouter, osqueryContext: OsqueryAppContex options: { tags: [`access:${PLUGIN_ID}-read`] }, }, async (context, request, response) => { - const esClient = context.core.elasticsearch.client.asInternalUser; - let agents; try { agents = await osqueryContext.service .getAgentService() + ?.asScoped(request) // @ts-expect-error update types - ?.listAgents(esClient, request.query); + .listAgents(request.query); } catch (error) { return response.badRequest({ body: error }); } diff --git a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts index 68ee826eca01c..79436c66d073f 100644 --- a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts +++ b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts @@ -34,6 +34,10 @@ import { EndpointAppContentServicesNotSetUpError, EndpointAppContentServicesNotStartedError, } from './errors'; +import { + EndpointFleetServicesFactory, + EndpointScopedFleetServicesInterface, +} from './services/endpoint_fleet_services'; export interface EndpointAppContextServiceSetupContract { securitySolutionRequestContextFactory: IRequestContextFactory; @@ -64,6 +68,7 @@ export type EndpointAppContextServiceStartContract = Partial< export class EndpointAppContextService { private setupDependencies: EndpointAppContextServiceSetupContract | null = null; private startDependencies: EndpointAppContextServiceStartContract | null = null; + private fleetServicesFactory: EndpointFleetServicesFactory | null = null; public security: SecurityPluginStart | undefined; public setup(dependencies: EndpointAppContextServiceSetupContract) { @@ -78,6 +83,17 @@ export class EndpointAppContextService { this.startDependencies = dependencies; this.security = dependencies.security; + // let's try to avoid turning off eslint's Forbidden non-null assertion rule + const { agentService, agentPolicyService, packagePolicyService, packageService } = + dependencies as Required; + + this.fleetServicesFactory = new EndpointFleetServicesFactory({ + agentService, + agentPolicyService, + packagePolicyService, + packageService, + }); + if (dependencies.registerIngestCallback && dependencies.manifestManager) { dependencies.registerIngestCallback( 'packagePolicyCreate', @@ -119,10 +135,20 @@ export class EndpointAppContextService { return this.startDependencies.endpointMetadataService; } + public getScopedFleetServices(req: KibanaRequest): EndpointScopedFleetServicesInterface { + if (this.fleetServicesFactory === null) { + throw new EndpointAppContentServicesNotStartedError(); + } + + return this.fleetServicesFactory.asScoped(req); + } + + /** @deprecated use `getScopedFleetServices()` instead */ public getAgentService(): AgentService | undefined { return this.startDependencies?.agentService; } + /** @deprecated use `getScopedFleetServices()` instead */ public getPackagePolicyService(): PackagePolicyServiceInterface { if (!this.startDependencies?.packagePolicyService) { throw new EndpointAppContentServicesNotStartedError(); @@ -130,6 +156,7 @@ export class EndpointAppContextService { return this.startDependencies?.packagePolicyService; } + /** @deprecated use `getScopedFleetServices()` instead */ public getAgentPolicyService(): AgentPolicyServiceInterface | undefined { return this.startDependencies?.agentPolicyService; } diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks.ts index 4c722672efe46..03e0b871a9240 100644 --- a/x-pack/plugins/security_solution/server/endpoint/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/mocks.ts @@ -17,7 +17,7 @@ import { createMockAgentPolicyService, createMockAgentService, createArtifactsClientMock, - fleetAuthzMock, + createFleetAuthzMock, } from '../../../fleet/server/mocks'; import { createMockConfig } from '../lib/detection_engine/routes/__mocks__'; import { @@ -96,7 +96,6 @@ export const createMockEndpointAppContextServiceStartContract = const packagePolicyService = createPackagePolicyServiceMock(); const endpointMetadataService = new EndpointMetadataService( savedObjectsStart, - agentService, agentPolicyService, packagePolicyService, logger @@ -155,7 +154,7 @@ export const createMockPackageService = (): jest.Mocked => { export const createMockFleetStartContract = (indexPattern: string): FleetStartContract => { return { authz: { - fromRequest: jest.fn().mockResolvedValue(fleetAuthzMock), + fromRequest: jest.fn().mockResolvedValue(createFleetAuthzMock()), }, fleetSetupCompleted: jest.fn().mockResolvedValue(undefined), esIndexPatternService: { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/enrichment.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/enrichment.test.ts index 9b454a266834c..8cbdbc064f8b9 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/enrichment.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/enrichment.test.ts @@ -9,6 +9,7 @@ import { HostStatus } from '../../../../common/endpoint/types'; import { createMockMetadataRequestContext } from '../../mocks'; import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; import { enrichHostMetadata, MetadataRequestContext } from './handlers'; +import { AgentClient } from '../../../../../fleet/server'; describe('test document enrichment', () => { let metaReqCtx: jest.Mocked; @@ -23,11 +24,9 @@ describe('test document enrichment', () => { beforeEach(() => { statusFn = jest.fn(); - (metaReqCtx.endpointAppContextService.getAgentService as jest.Mock).mockImplementation(() => { - return { - getAgentStatusById: statusFn, - }; - }); + metaReqCtx.requestHandlerContext!.fleet!.agentClient.asCurrentUser = { + getAgentStatusById: statusFn, + } as unknown as AgentClient; }); it('should return host healthy for online agent', async () => { @@ -87,12 +86,10 @@ describe('test document enrichment', () => { beforeEach(() => { agentMock = jest.fn(); agentPolicyMock = jest.fn(); - (metaReqCtx.endpointAppContextService.getAgentService as jest.Mock).mockImplementation(() => { - return { - getAgent: agentMock, - getAgentStatusById: jest.fn(), - }; - }); + metaReqCtx.requestHandlerContext!.fleet!.agentClient.asCurrentUser = { + getAgent: agentMock, + getAgentStatusById: jest.fn(), + } as unknown as AgentClient; (metaReqCtx.endpointAppContextService.getAgentPolicyService as jest.Mock).mockImplementation( () => { return { 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 63e56af3fec6f..708cac5a845ce 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 @@ -35,7 +35,6 @@ import { EndpointAppContextService } from '../../endpoint_app_context_services'; import { fleetAgentStatusToEndpointHostStatus } from '../../utils'; import { queryResponseToHostListResult } from './support/query_strategies'; import { NotFoundError } from '../../errors'; -import { EndpointError } from '../../../../common/endpoint/errors'; import { EndpointHostUnEnrolledError } from '../../services/metadata'; import { CustomHttpRequestError } from '../../../utils/custom_http_request_error'; import { GetMetadataListRequestQuery } from '../../../../common/endpoint/schema/metadata'; @@ -43,6 +42,7 @@ import { ENDPOINT_DEFAULT_PAGE, ENDPOINT_DEFAULT_PAGE_SIZE, } from '../../../../common/endpoint/constants'; +import { EndpointFleetServicesInterface } from '../../services/endpoint_fleet_services'; export interface MetadataRequestContext { esClient?: IScopedClusterClient; @@ -93,9 +93,7 @@ export const getMetadataListRequestHandler = function ( > { return async (context, request, response) => { const endpointMetadataService = endpointAppContext.service.getEndpointMetadataService(); - if (!endpointMetadataService) { - throw new EndpointError('endpoint metadata service not available'); - } + const fleetServices = endpointAppContext.service.getScopedFleetServices(request); let doesUnitedIndexExist = false; let didUnitedIndexError = false; @@ -118,18 +116,25 @@ export const getMetadataListRequestHandler = function ( // If no unified Index present, then perform a search using the legacy approach if (!doesUnitedIndexExist || didUnitedIndexError) { const endpointPolicies = await getAllEndpointPackagePolicies( - endpointAppContext.service.getPackagePolicyService(), + fleetServices.packagePolicy, context.core.savedObjects.client ); const pagingProperties = await getPagingProperties(request, endpointAppContext); - body = await legacyListMetadataQuery(context, endpointAppContext, logger, endpointPolicies, { - page: pagingProperties.pageIndex, - pageSize: pagingProperties.pageSize, - kuery: request?.body?.filters?.kql || '', - hostStatuses: request?.body?.filters?.host_status || [], - }); + body = await legacyListMetadataQuery( + context, + endpointAppContext, + fleetServices, + logger, + endpointPolicies, + { + page: pagingProperties.pageIndex, + pageSize: pagingProperties.pageSize, + kuery: request?.body?.filters?.kql || '', + hostStatuses: request?.body?.filters?.host_status || [], + } + ); return response.ok({ body }); } @@ -138,6 +143,7 @@ export const getMetadataListRequestHandler = function ( const pagingProperties = await getPagingProperties(request, endpointAppContext); const { data, total } = await endpointMetadataService.getHostMetadataList( context.core.elasticsearch.client.asCurrentUser, + fleetServices, { page: pagingProperties.pageIndex, pageSize: pagingProperties.pageSize, @@ -171,6 +177,7 @@ export function getMetadataListRequestHandlerV2( > { return async (context, request, response) => { const endpointMetadataService = endpointAppContext.service.getEndpointMetadataService(); + const fleetServices = endpointAppContext.service.getScopedFleetServices(request); let doesUnitedIndexExist = false; let didUnitedIndexError = false; @@ -193,13 +200,14 @@ export function getMetadataListRequestHandlerV2( // If no unified Index present, then perform a search using the legacy approach if (!doesUnitedIndexExist || didUnitedIndexError) { const endpointPolicies = await getAllEndpointPackagePolicies( - endpointAppContext.service.getPackagePolicyService(), + fleetServices.packagePolicy, context.core.savedObjects.client ); const legacyResponse = await legacyListMetadataQuery( context, endpointAppContext, + fleetServices, logger, endpointPolicies, request.query @@ -217,6 +225,7 @@ export function getMetadataListRequestHandlerV2( try { const { data, total } = await endpointMetadataService.getHostMetadataList( context.core.elasticsearch.client.asCurrentUser, + fleetServices, request.query ); @@ -250,6 +259,7 @@ export const getMetadataRequestHandler = function ( return response.ok({ body: await endpointMetadataService.getEnrichedHostMetadata( context.core.elasticsearch.client.asCurrentUser, + endpointAppContext.service.getScopedFleetServices(request), request.params.id ), }); @@ -314,10 +324,6 @@ export async function enrichHostMetadata( throw e; } - const esClient = (metadataRequestContext?.esClient ?? - metadataRequestContext.requestHandlerContext?.core.elasticsearch - .client) as IScopedClusterClient; - const esSavedObjectClient = metadataRequestContext?.savedObjectsClient ?? (metadataRequestContext.requestHandlerContext?.core.savedObjects @@ -333,9 +339,10 @@ export async function enrichHostMetadata( log.warn(`Missing elastic agent id, using host id instead ${elasticAgentId}`); } - const status = await metadataRequestContext.endpointAppContextService - ?.getAgentService() - ?.getAgentStatusById(esClient.asCurrentUser, elasticAgentId); + const status = + await metadataRequestContext.requestHandlerContext?.fleet?.agentClient.asCurrentUser.getAgentStatusById( + elasticAgentId + ); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion hostStatus = fleetAgentStatusToEndpointHostStatus(status!); } catch (e) { @@ -349,9 +356,10 @@ export async function enrichHostMetadata( let policyInfo: HostInfo['policy_info']; try { - const agent = await metadataRequestContext.endpointAppContextService - ?.getAgentService() - ?.getAgent(esClient.asCurrentUser, elasticAgentId); + const agent = + await metadataRequestContext.requestHandlerContext?.fleet?.agentClient.asCurrentUser.getAgent( + elasticAgentId + ); const agentPolicy = await metadataRequestContext.endpointAppContextService .getAgentPolicyService() // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -393,15 +401,12 @@ export async function enrichHostMetadata( async function legacyListMetadataQuery( context: SecuritySolutionRequestHandlerContext, endpointAppContext: EndpointAppContext, + fleetServices: EndpointFleetServicesInterface, logger: Logger, endpointPolicies: PackagePolicy[], queryOptions: GetMetadataListRequestQuery ): Promise { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const agentService = endpointAppContext.service.getAgentService()!; - if (agentService === undefined) { - throw new Error('agentService not available'); - } + const fleetAgentClient = fleetServices.agent; const metadataRequestContext: MetadataRequestContext = { esClient: context.core.elasticsearch.client, @@ -412,14 +417,15 @@ async function legacyListMetadataQuery( }; const endpointPolicyIds = endpointPolicies.map((policy) => policy.policy_id); + const unenrolledAgentIds = await findAllUnenrolledAgentIds( - agentService, + fleetAgentClient, context.core.elasticsearch.client.asCurrentUser, endpointPolicyIds ); const statusAgentIds = await findAgentIdsByStatus( - agentService, + fleetAgentClient, context.core.elasticsearch.client.asCurrentUser, queryOptions?.hostStatuses || [] ); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts index c1dfee0252b38..c705246014a7b 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts @@ -43,7 +43,7 @@ import { legacyMetadataSearchResponseMock, unitedMetadataSearchResponseMock, } from './support/test_support'; -import { PackageService } from '../../../../../fleet/server/services'; +import { AgentClient, PackageService } from '../../../../../fleet/server/services'; import { HOST_METADATA_GET_ROUTE, HOST_METADATA_LIST_ROUTE, @@ -60,6 +60,7 @@ import { } from '../../../../../../../src/core/server/elasticsearch/client/mocks'; import { EndpointHostNotFoundError } from '../../services/metadata'; import { FleetAgentGenerator } from '../../../../common/endpoint/data_generators/fleet_agent_generator'; +import { createMockAgentClient } from '../../../../../fleet/server/mocks'; class IndexNotFoundException extends Error { meta: { body: { error: { type: string } } }; @@ -88,6 +89,7 @@ describe('test endpoint routes', () => { let mockAgentPolicyService: Required< ReturnType >['agentPolicyService']; + let mockAgentClient: jest.Mocked; let endpointAppContextService: EndpointAppContextService; let startContract: EndpointAppContextServiceStartContract; const noUnenrolledAgent = { @@ -151,6 +153,8 @@ describe('test endpoint routes', () => { endpointAppContextService.setup(createMockEndpointAppContextServiceSetupContract()); endpointAppContextService.start({ ...startContract, packageService: mockPackageService }); mockAgentService = startContract.agentService!; + mockAgentClient = createMockAgentClient(); + mockAgentService.asScoped = () => mockAgentClient; mockAgentPolicyService = startContract.agentPolicyService!; registerEndpointRoutes(routerMock, { @@ -176,8 +180,8 @@ describe('test endpoint routes', () => { [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith(HOST_METADATA_LIST_ROUTE) )!; - mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); - mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent); + mockAgentClient.getAgentStatusById.mockResolvedValue('error'); + mockAgentClient.listAgents.mockResolvedValue(noUnenrolledAgent); await routeHandler( createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), mockRequest, @@ -220,8 +224,8 @@ describe('test endpoint routes', () => { }, }); - mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); - mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent); + mockAgentClient.getAgentStatusById.mockResolvedValue('error'); + mockAgentClient.listAgents.mockResolvedValue(noUnenrolledAgent); mockAgentPolicyService.getByIds = jest.fn().mockResolvedValueOnce([]); const metadata = new EndpointDocGenerator().generateHostMetadata(); const esSearchMock = mockScopedClient.asCurrentUser.search as jest.Mock; @@ -415,6 +419,8 @@ describe('test endpoint routes', () => { endpointAppContextService.setup(createMockEndpointAppContextServiceSetupContract()); endpointAppContextService.start({ ...startContract, packageService: mockPackageService }); mockAgentService = startContract.agentService!; + mockAgentClient = createMockAgentClient(); + mockAgentService.asScoped = () => mockAgentClient; registerEndpointRoutes(routerMock, { logFactory: loggingSystemMock.create(), @@ -439,8 +445,8 @@ describe('test endpoint routes', () => { [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith(HOST_METADATA_LIST_ROUTE) )!; - mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); - mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent); + mockAgentClient.getAgentStatusById.mockResolvedValue('error'); + mockAgentClient.listAgents.mockResolvedValue(noUnenrolledAgent); await routeHandler( createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), mockRequest, @@ -474,8 +480,8 @@ describe('test endpoint routes', () => { }, }); - mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); - mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent); + mockAgentClient.getAgentStatusById.mockResolvedValue('error'); + mockAgentClient.listAgents.mockResolvedValue(noUnenrolledAgent); (mockScopedClient.asCurrentUser.search as jest.Mock) .mockImplementationOnce(() => { throw new IndexNotFoundException(); @@ -536,8 +542,8 @@ describe('test endpoint routes', () => { }, }); - mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); - mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent); + mockAgentClient.getAgentStatusById.mockResolvedValue('error'); + mockAgentClient.listAgents.mockResolvedValue(noUnenrolledAgent); (mockScopedClient.asCurrentUser.search as jest.Mock) .mockImplementationOnce(() => { throw new IndexNotFoundException(); @@ -653,6 +659,8 @@ describe('test endpoint routes', () => { endpointAppContextService.setup(createMockEndpointAppContextServiceSetupContract()); endpointAppContextService.start({ ...startContract, packageService: mockPackageService }); mockAgentService = startContract.agentService!; + mockAgentClient = createMockAgentClient(); + mockAgentService.asScoped = () => mockAgentClient; mockAgentPolicyService = startContract.agentPolicyService!; registerEndpointRoutes(routerMock, { @@ -683,8 +691,8 @@ describe('test endpoint routes', () => { [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith(HOST_METADATA_LIST_ROUTE) )!; - mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); - mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent); + mockAgentClient.getAgentStatusById.mockResolvedValue('error'); + mockAgentClient.listAgents.mockResolvedValue(noUnenrolledAgent); await routeHandler( createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), mockRequest, @@ -718,8 +726,8 @@ describe('test endpoint routes', () => { }, }); - mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); - mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent); + mockAgentClient.getAgentStatusById.mockResolvedValue('error'); + mockAgentClient.listAgents.mockResolvedValue(noUnenrolledAgent); mockAgentPolicyService.getByIds = jest.fn().mockResolvedValueOnce([]); const metadata = new EndpointDocGenerator().generateHostMetadata(); const esSearchMock = mockScopedClient.asCurrentUser.search as jest.Mock; @@ -913,6 +921,8 @@ describe('test endpoint routes', () => { endpointAppContextService.setup(createMockEndpointAppContextServiceSetupContract()); endpointAppContextService.start({ ...startContract, packageService: mockPackageService }); mockAgentService = startContract.agentService!; + mockAgentClient = createMockAgentClient(); + mockAgentService.asScoped = () => mockAgentClient; registerEndpointRoutes(routerMock, { logFactory: loggingSystemMock.create(), @@ -942,8 +952,8 @@ describe('test endpoint routes', () => { [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith(HOST_METADATA_LIST_ROUTE) )!; - mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); - mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent); + mockAgentClient.getAgentStatusById.mockResolvedValue('error'); + mockAgentClient.listAgents.mockResolvedValue(noUnenrolledAgent); await routeHandler( createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), mockRequest, @@ -971,8 +981,8 @@ describe('test endpoint routes', () => { }, }); - mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); - mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent); + mockAgentClient.getAgentStatusById.mockResolvedValue('error'); + mockAgentClient.listAgents.mockResolvedValue(noUnenrolledAgent); (mockScopedClient.asCurrentUser.search as jest.Mock) .mockImplementationOnce(() => { throw new IndexNotFoundException(); @@ -1026,8 +1036,8 @@ describe('test endpoint routes', () => { }, }); - mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); - mockAgentService.listAgents = jest.fn().mockReturnValue(noUnenrolledAgent); + mockAgentClient.getAgentStatusById.mockResolvedValue('error'); + mockAgentClient.listAgents.mockResolvedValue(noUnenrolledAgent); (mockScopedClient.asCurrentUser.search as jest.Mock) .mockImplementationOnce(() => { throw new IndexNotFoundException(); @@ -1142,6 +1152,8 @@ describe('test endpoint routes', () => { endpointAppContextService.setup(createMockEndpointAppContextServiceSetupContract()); endpointAppContextService.start({ ...startContract, packageService: mockPackageService }); mockAgentService = startContract.agentService!; + mockAgentClient = createMockAgentClient(); + mockAgentService.asScoped = () => mockAgentClient; registerEndpointRoutes(routerMock, { logFactory: loggingSystemMock.create(), @@ -1160,8 +1172,8 @@ describe('test endpoint routes', () => { Promise.resolve({ body: legacyMetadataSearchResponseMock() }) ); - mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); - mockAgentService.getAgent = jest.fn().mockReturnValue({ + mockAgentClient.getAgentStatusById.mockResolvedValue('error'); + mockAgentClient.getAgent.mockResolvedValue({ active: true, } as unknown as Agent); @@ -1192,9 +1204,7 @@ describe('test endpoint routes', () => { params: { id: response.hits.hits[0]._id }, }); - mockAgentService.getAgent = jest - .fn() - .mockReturnValue(agentGenerator.generate({ status: 'online' })); + mockAgentClient.getAgent.mockResolvedValue(agentGenerator.generate({ status: 'online' })); (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => Promise.resolve({ body: response }) ); @@ -1229,7 +1239,7 @@ describe('test endpoint routes', () => { params: { id: response.hits.hits[0]._id }, }); - mockAgentService.getAgent = jest.fn().mockRejectedValue(new AgentNotFoundError('not found')); + mockAgentClient.getAgent.mockRejectedValue(new AgentNotFoundError('not found')); (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => Promise.resolve({ body: response }) @@ -1264,7 +1274,7 @@ describe('test endpoint routes', () => { params: { id: response.hits.hits[0]._id }, }); - mockAgentService.getAgent = jest.fn().mockReturnValue( + mockAgentClient.getAgent.mockResolvedValue( agentGenerator.generate({ status: 'error', }) @@ -1304,7 +1314,7 @@ describe('test endpoint routes', () => { (mockScopedClient.asCurrentUser.search as jest.Mock).mockImplementationOnce(() => Promise.resolve({ body: response }) ); - mockAgentService.getAgent = jest.fn().mockReturnValue({ + mockAgentClient.getAgent.mockResolvedValue({ active: false, } as unknown as Agent); 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 1eb9cfaf109a8..41286e44b1bd4 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 @@ -8,21 +8,21 @@ import { ElasticsearchClient } from 'kibana/server'; import { buildStatusesKuery, findAgentIdsByStatus } from './agent_status'; import { elasticsearchServiceMock } from '../../../../../../../../src/core/server/mocks'; -import { AgentService } from '../../../../../../fleet/server/services'; -import { createMockAgentService } from '../../../../../../fleet/server/mocks'; +import { AgentClient } from '../../../../../../fleet/server/services'; +import { createMockAgentClient } from '../../../../../../fleet/server/mocks'; import { Agent } from '../../../../../../fleet/common/types/models'; import { AgentStatusKueryHelper } from '../../../../../../fleet/common/services'; describe('test filtering endpoint hosts by agent status', () => { let mockElasticsearchClient: jest.Mocked; - let mockAgentService: jest.Mocked; + let mockAgentClient: jest.Mocked; beforeEach(() => { mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - mockAgentService = createMockAgentService(); + mockAgentClient = createMockAgentClient(); }); it('will accept a valid status condition', async () => { - mockAgentService.listAgents.mockImplementationOnce(() => + mockAgentClient.listAgents.mockImplementationOnce(() => Promise.resolve({ agents: [], total: 0, @@ -31,14 +31,14 @@ describe('test filtering endpoint hosts by agent status', () => { }) ); - const result = await findAgentIdsByStatus(mockAgentService, mockElasticsearchClient, [ + const result = await findAgentIdsByStatus(mockAgentClient, mockElasticsearchClient, [ 'healthy', ]); expect(result).toBeDefined(); }); it('will filter for offline hosts', async () => { - mockAgentService.listAgents + mockAgentClient.listAgents .mockImplementationOnce(() => Promise.resolve({ agents: [{ id: 'id1' } as unknown as Agent, { id: 'id2' } as unknown as Agent], @@ -56,11 +56,11 @@ describe('test filtering endpoint hosts by agent status', () => { }) ); - const result = await findAgentIdsByStatus(mockAgentService, mockElasticsearchClient, [ + const result = await findAgentIdsByStatus(mockAgentClient, mockElasticsearchClient, [ 'offline', ]); const offlineKuery = AgentStatusKueryHelper.buildKueryForOfflineAgents(); - expect(mockAgentService.listAgents.mock.calls[0][1].kuery).toEqual( + expect(mockAgentClient.listAgents.mock.calls[0][0].kuery).toEqual( expect.stringContaining(offlineKuery) ); expect(result).toBeDefined(); @@ -68,7 +68,7 @@ describe('test filtering endpoint hosts by agent status', () => { }); it('will filter for multiple statuses', async () => { - mockAgentService.listAgents + mockAgentClient.listAgents .mockImplementationOnce(() => Promise.resolve({ agents: [{ id: 'A' } as unknown as Agent, { id: 'B' } as unknown as Agent], @@ -86,13 +86,13 @@ describe('test filtering endpoint hosts by agent status', () => { }) ); - const result = await findAgentIdsByStatus(mockAgentService, mockElasticsearchClient, [ + const result = await findAgentIdsByStatus(mockAgentClient, mockElasticsearchClient, [ 'updating', 'unhealthy', ]); const unenrollKuery = AgentStatusKueryHelper.buildKueryForUpdatingAgents(); const errorKuery = AgentStatusKueryHelper.buildKueryForErrorAgents(); - expect(mockAgentService.listAgents.mock.calls[0][1].kuery).toEqual( + expect(mockAgentClient.listAgents.mock.calls[0][0].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 f9e04f4edebee..73bdf3c7c3a81 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 @@ -6,7 +6,7 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { AgentService } from '../../../../../../fleet/server'; +import { AgentClient } from '../../../../../../fleet/server'; import { AgentStatusKueryHelper } from '../../../../../../fleet/common/services'; import { Agent } from '../../../../../../fleet/common/types/models'; import { HostStatus } from '../../../../../common/endpoint/types'; @@ -34,7 +34,7 @@ export function buildStatusesKuery(statusesToFilter: string[]): string | undefin } export async function findAgentIdsByStatus( - agentService: AgentService, + agentClient: AgentClient, esClient: ElasticsearchClient, statuses: string[], pageSize: number = 1000 @@ -59,7 +59,7 @@ export async function findAgentIdsByStatus( let hasMore = true; while (hasMore) { - const agents = await agentService.listAgents(esClient, searchOptions(page++)); + const agents = await agentClient.listAgents(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 6efac10b94fef..1ae9608ee9397 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 @@ -8,9 +8,9 @@ import { ElasticsearchClient } from 'kibana/server'; import { findAllUnenrolledAgentIds } from './unenroll'; import { elasticsearchServiceMock } from '../../../../../../../../src/core/server/mocks'; -import { AgentService } from '../../../../../../fleet/server/services'; +import { AgentClient } from '../../../../../../fleet/server/services'; import { - createMockAgentService, + createMockAgentClient, createPackagePolicyServiceMock, } from '../../../../../../fleet/server/mocks'; import { Agent, PackagePolicy } from '../../../../../../fleet/common/types/models'; @@ -18,12 +18,12 @@ import { PackagePolicyServiceInterface } from '../../../../../../fleet/server'; describe('test find all unenrolled Agent id', () => { let mockElasticsearchClient: jest.Mocked; - let mockAgentService: jest.Mocked; + let mockAgentClient: jest.Mocked; let mockPackagePolicyService: jest.Mocked; beforeEach(() => { mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - mockAgentService = createMockAgentService(); + mockAgentClient = createMockAgentClient(); mockPackagePolicyService = createPackagePolicyServiceMock(); }); @@ -46,7 +46,7 @@ describe('test find all unenrolled Agent id', () => { perPage: 10, page: 1, }); - mockAgentService.listAgents + mockAgentClient.listAgents .mockImplementationOnce(() => Promise.resolve({ agents: [ @@ -81,7 +81,7 @@ describe('test find all unenrolled Agent id', () => { ); const endpointPolicyIds = ['test-endpoint-policy-id']; const agentIds = await findAllUnenrolledAgentIds( - mockAgentService, + mockAgentClient, mockElasticsearchClient, endpointPolicyIds ); @@ -89,7 +89,7 @@ describe('test find all unenrolled Agent id', () => { expect(agentIds).toBeTruthy(); expect(agentIds).toEqual(['id1', 'id2']); - expect(mockAgentService.listAgents).toHaveBeenNthCalledWith(1, mockElasticsearchClient, { + expect(mockAgentClient.listAgents).toHaveBeenNthCalledWith(1, { page: 1, perPage: 1000, showInactive: true, 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 2af1c9a597ebb..99559f6a1b8f0 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 @@ -6,11 +6,11 @@ */ import { ElasticsearchClient } from 'kibana/server'; -import { AgentService } from '../../../../../../fleet/server'; +import type { AgentClient } from '../../../../../../fleet/server'; import { Agent } from '../../../../../../fleet/common/types/models'; export async function findAllUnenrolledAgentIds( - agentService: AgentService, + agentClient: AgentClient, esClient: ElasticsearchClient, endpointPolicyIds: string[], pageSize: number = 1000 @@ -41,7 +41,7 @@ export async function findAllUnenrolledAgentIds( let hasMore = true; while (hasMore) { - const unenrolledAgents = await agentService.listAgents(esClient, searchOptions(page++)); + const unenrolledAgents = await agentClient.listAgents(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.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts index 0570eeb708d4e..b8efa2636d8c7 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/policy/handlers.test.ts @@ -11,7 +11,7 @@ import { createMockEndpointAppContextServiceStartContract, createRouteHandlerContext, } from '../../mocks'; -import { createMockAgentService } from '../../../../../fleet/server/mocks'; +import { createMockAgentClient, createMockAgentService } from '../../../../../fleet/server/mocks'; import { getHostPolicyResponseHandler, getAgentPolicySummaryHandler } from './handlers'; import { KibanaResponseFactory, @@ -29,7 +29,7 @@ import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data' import { parseExperimentalConfigValue } from '../../../../common/experimental_features'; import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__'; import { Agent } from '../../../../../fleet/common/types/models'; -import { AgentService } from '../../../../../fleet/server/services'; +import { AgentClient, AgentService } from '../../../../../fleet/server/services'; import { get } from 'lodash'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ScopedClusterClientMock } from '../../../../../../../src/core/server/elasticsearch/client/mocks'; @@ -101,6 +101,7 @@ describe('test policy response handler', () => { describe('test agent policy summary handler', () => { let mockAgentService: jest.Mocked; + let mockAgentClient: jest.Mocked; let agentListResult: { agents: Agent[]; @@ -122,6 +123,8 @@ describe('test policy response handler', () => { mockResponse = httpServerMock.createResponseFactory(); endpointAppContextService = new EndpointAppContextService(); mockAgentService = createMockAgentService(); + mockAgentClient = createMockAgentClient(); + mockAgentService.asScoped.mockReturnValue(mockAgentClient); emptyAgentListResult = { agents: [], total: 2, @@ -173,7 +176,7 @@ describe('test policy response handler', () => { afterEach(() => endpointAppContextService.stop()); it('should return the summary of all the agent with the given policy name', async () => { - mockAgentService.listAgents + mockAgentClient.listAgents .mockImplementationOnce(() => Promise.resolve(agentListResult)) .mockImplementationOnce(() => Promise.resolve(emptyAgentListResult)); @@ -204,7 +207,7 @@ describe('test policy response handler', () => { }); it('should return the agent summary', async () => { - mockAgentService.listAgents + mockAgentClient.listAgents .mockImplementationOnce(() => Promise.resolve(agentListResult)) .mockImplementationOnce(() => Promise.resolve(emptyAgentListResult)); 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 45b6201c47773..0e40836c28337 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 ( endpointAppContext, context.core.savedObjects.client, context.core.elasticsearch.client.asCurrentUser, + request, 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 ced0e00d34585..5d3e862611b0f 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 @@ -8,6 +8,7 @@ import { ElasticsearchClient, IScopedClusterClient, + KibanaRequest, SavedObjectsClientContract, } from '../../../../../../../src/core/server'; import { GetHostPolicyResponse, HostPolicyResponse } from '../../../../common/endpoint/types'; @@ -78,6 +79,7 @@ export async function getAgentPolicySummary( endpointAppContext: EndpointAppContext, soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, + request: KibanaRequest, packageName: string, policyId?: string, pageSize: number = 1000 @@ -89,6 +91,7 @@ export async function getAgentPolicySummary( endpointAppContext, soClient, esClient, + request, `${agentQuery} AND policy_id:${policyId}`, pageSize ) @@ -96,7 +99,7 @@ export async function getAgentPolicySummary( } return transformAgentVersionMap( - await agentVersionsMap(endpointAppContext, soClient, esClient, agentQuery, pageSize) + await agentVersionsMap(endpointAppContext, soClient, esClient, request, agentQuery, pageSize) ); } @@ -104,6 +107,7 @@ export async function agentVersionsMap( endpointAppContext: EndpointAppContext, soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, + request: KibanaRequest, kqlQuery: string, pageSize: number = 1000 ): Promise> { @@ -123,7 +127,8 @@ export async function agentVersionsMap( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const queryResult = await endpointAppContext.service .getAgentService()! - .listAgents(esClient, searchOptions(page++)); + .asScoped(request) + .listAgents(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/endpoint_fleet_services.ts b/x-pack/plugins/security_solution/server/endpoint/services/endpoint_fleet_services.ts new file mode 100644 index 0000000000000..0c26582f920b1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/services/endpoint_fleet_services.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { KibanaRequest } from 'kibana/server'; +import type { + AgentClient, + AgentPolicyServiceInterface, + FleetStartContract, + PackagePolicyServiceInterface, + PackageService, +} from '../../../../fleet/server'; + +export interface EndpointFleetServicesFactoryInterface { + asScoped(req: KibanaRequest): EndpointScopedFleetServicesInterface; + + asInternalUser(): EndpointInternalFleetServicesInterface; +} + +export class EndpointFleetServicesFactory implements EndpointFleetServicesFactoryInterface { + constructor( + private readonly fleetDependencies: Pick< + FleetStartContract, + 'agentService' | 'packageService' | 'packagePolicyService' | 'agentPolicyService' + > + ) {} + + asScoped(req: KibanaRequest): EndpointScopedFleetServicesInterface { + const { + agentPolicyService: agentPolicy, + packagePolicyService: packagePolicy, + agentService, + packageService: packages, + } = this.fleetDependencies; + + return { + agent: agentService.asScoped(req), + agentPolicy, + packages, + packagePolicy, + + asInternal: this.asInternalUser.bind(this), + }; + } + + asInternalUser(): EndpointInternalFleetServicesInterface { + const { + agentPolicyService: agentPolicy, + packagePolicyService: packagePolicy, + agentService, + packageService: packages, + } = this.fleetDependencies; + + return { + agent: agentService.asInternalUser, + agentPolicy, + packages, + packagePolicy, + + asScoped: this.asScoped.bind(this), + }; + } +} + +/** + * The set of Fleet services used by Endpoint + */ +export interface EndpointFleetServicesInterface { + agent: AgentClient; + agentPolicy: AgentPolicyServiceInterface; + packages: PackageService; + packagePolicy: PackagePolicyServiceInterface; +} + +export interface EndpointScopedFleetServicesInterface extends EndpointFleetServicesInterface { + /** + * get internal fleet services instance + */ + asInternal: EndpointFleetServicesFactoryInterface['asInternalUser']; +} + +export interface EndpointInternalFleetServicesInterface extends EndpointFleetServicesInterface { + /** + * get scoped endpoint fleet services instance + */ + asScoped: EndpointFleetServicesFactoryInterface['asScoped']; +} diff --git a/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.test.ts index cba94cce83232..db38598ea6dd1 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.test.ts @@ -117,12 +117,16 @@ describe('EndpointMetadataService', () => { it('should throw wrapped error if es error', async () => { const esMockResponse = elasticsearchServiceMock.createErrorTransportRequestPromise({}); esClient.search.mockResolvedValue(esMockResponse); - const metadataListResponse = metadataService.getHostMetadataList(esClient, { - page: 0, - pageSize: 10, - kuery: '', - hostStatuses: [], - }); + const metadataListResponse = metadataService.getHostMetadataList( + esClient, + testMockedContext.fleetServices, + { + page: 0, + pageSize: 10, + kuery: '', + hostStatuses: [], + } + ); await expect(metadataListResponse).rejects.toThrow(EndpointError); }); @@ -176,6 +180,7 @@ describe('EndpointMetadataService', () => { const queryOptions = { page: 1, pageSize: 10, kuery: '', hostStatuses: [] }; const metadataListResponse = await metadataService.getHostMetadataList( esClient, + testMockedContext.fleetServices, queryOptions ); const unitedIndexQuery = await buildUnitedIndexQuery(queryOptions, packagePolicyIds); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.ts b/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.ts index 00bc0618f57ad..86f4fd28f506a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/metadata/endpoint_metadata_service.ts @@ -26,7 +26,6 @@ import { Agent, AgentPolicy, PackagePolicy } from '../../../../../fleet/common'; import { AgentNotFoundError, AgentPolicyServiceInterface, - AgentService, PackagePolicyServiceInterface, } from '../../../../../fleet/server'; import { @@ -57,6 +56,7 @@ import { getAllEndpointPackagePolicies } from '../../routes/metadata/support/end import { getAgentStatus } from '../../../../../fleet/common/services/agent_status'; import { GetMetadataListRequestQuery } from '../../../../common/endpoint/schema/metadata'; import { EndpointError } from '../../../../common/endpoint/errors'; +import { EndpointFleetServicesInterface } from '../endpoint_fleet_services'; type AgentPolicyWithPackagePolicies = Omit & { package_policies: PackagePolicy[]; @@ -84,7 +84,6 @@ export class EndpointMetadataService { constructor( private savedObjectsStart: SavedObjectsServiceStart, - private readonly agentService: AgentService, private readonly agentPolicyService: AgentPolicyServiceInterface, private readonly packagePolicyService: PackagePolicyServiceInterface, private readonly logger?: Logger @@ -156,12 +155,14 @@ export class EndpointMetadataService { * Retrieve a single endpoint host metadata along with fleet information * * @param esClient Elasticsearch Client (usually scoped to the user's context) + * @param fleetServices * @param endpointId the endpoint id (from `agent.id`) * * @throws */ async getEnrichedHostMetadata( esClient: ElasticsearchClient, + fleetServices: EndpointFleetServicesInterface, endpointId: string ): Promise { const endpointMetadata = await this.getHostMetadata(esClient, endpointId); @@ -176,7 +177,7 @@ export class EndpointMetadataService { this.logger?.warn(`Missing elastic agent id, using host id instead ${fleetAgentId}`); } - fleetAgent = await this.getFleetAgent(esClient, fleetAgentId); + fleetAgent = await this.getFleetAgent(fleetServices.agent, fleetAgentId); } catch (error) { if (error instanceof FleetAgentNotFoundError) { this.logger?.warn(`agent with id ${fleetAgentId} not found`); @@ -192,12 +193,12 @@ export class EndpointMetadataService { ); } - return this.enrichHostMetadata(esClient, endpointMetadata, fleetAgent); + return this.enrichHostMetadata(fleetServices, endpointMetadata, fleetAgent); } /** * Enriches a host metadata document with data from fleet - * @param esClient + * @param fleetServices * @param endpointMetadata * @param _fleetAgent * @param _fleetAgentPolicy @@ -206,7 +207,7 @@ export class EndpointMetadataService { */ // eslint-disable-next-line complexity private async enrichHostMetadata( - esClient: ElasticsearchClient, + fleetServices: EndpointFleetServicesInterface, endpointMetadata: HostMetadata, /** * If undefined, it will be retrieved from Fleet using the ID in the endpointMetadata. @@ -242,7 +243,7 @@ export class EndpointMetadataService { ); } - fleetAgent = await this.getFleetAgent(esClient, fleetAgentId); + fleetAgent = await this.getFleetAgent(fleetServices.agent, fleetAgentId); } catch (error) { if (error instanceof FleetAgentNotFoundError) { this.logger?.warn(`agent with id ${fleetAgentId} not found`); @@ -310,12 +311,15 @@ export class EndpointMetadataService { /** * Retrieve a single Fleet Agent data * - * @param esClient Elasticsearch Client (usually scoped to the user's context) + * @param fleetAgentService * @param agentId The elastic agent id (`from `elastic.agent.id`) */ - async getFleetAgent(esClient: ElasticsearchClient, agentId: string): Promise { + async getFleetAgent( + fleetAgentService: EndpointFleetServicesInterface['agent'], + agentId: string + ): Promise { try { - return await this.agentService.getAgent(esClient, agentId); + return await fleetAgentService.getAgent(agentId); } catch (error) { if (error instanceof AgentNotFoundError) { throw new FleetAgentNotFoundError(`agent with id ${agentId} not found`, error); @@ -402,6 +406,7 @@ export class EndpointMetadataService { */ async getHostMetadataList( esClient: ElasticsearchClient, + fleetServices: EndpointFleetServicesInterface, queryOptions: GetMetadataListRequestQuery ): Promise> { const endpointPolicies = await getAllEndpointPackagePolicies( @@ -468,7 +473,7 @@ export class EndpointMetadataService { const endpointPolicy = endpointPoliciesMap[agent.policy_id!]; hosts.push( - await this.enrichHostMetadata(esClient, metadata, agent, agentPolicy, endpointPolicy) + await this.enrichHostMetadata(fleetServices, metadata, agent, agentPolicy, endpointPolicy) ); } } diff --git a/x-pack/plugins/security_solution/server/endpoint/services/metadata/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/services/metadata/mocks.ts index 166f834500927..42b0f4f44fdf8 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/metadata/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/metadata/mocks.ts @@ -11,9 +11,14 @@ import { savedObjectsServiceMock } from '../../../../../../../src/core/server/mo import { createMockAgentPolicyService, createMockAgentService, + createMockPackageService, createPackagePolicyServiceMock, } from '../../../../../fleet/server/mocks'; import { AgentPolicyServiceInterface, AgentService } from '../../../../../fleet/server'; +import { + EndpointFleetServicesFactory, + EndpointFleetServicesInterface, +} from '../endpoint_fleet_services'; const createCustomizedPackagePolicyService = () => { const service = createPackagePolicyServiceMock(); @@ -38,6 +43,7 @@ export interface EndpointMetadataServiceTestContextMock { agentPolicyService: jest.Mocked; packagePolicyService: ReturnType; endpointMetadataService: EndpointMetadataService; + fleetServices: EndpointFleetServicesInterface; } export const createEndpointMetadataServiceTestContextMock = ( @@ -46,11 +52,18 @@ export const createEndpointMetadataServiceTestContextMock = ( agentPolicyService: jest.Mocked = createMockAgentPolicyService(), packagePolicyService: ReturnType< typeof createPackagePolicyServiceMock - > = createCustomizedPackagePolicyService() + > = createCustomizedPackagePolicyService(), + packageService: ReturnType = createMockPackageService() ): EndpointMetadataServiceTestContextMock => { + const fleetServices = new EndpointFleetServicesFactory({ + agentService, + packageService, + packagePolicyService, + agentPolicyService, + }).asInternalUser(); + const endpointMetadataService = new EndpointMetadataService( savedObjectsStart, - agentService, agentPolicyService, packagePolicyService ); @@ -61,5 +74,6 @@ export const createEndpointMetadataServiceTestContextMock = ( agentPolicyService, packagePolicyService, endpointMetadataService, + fleetServices, }; }; diff --git a/x-pack/plugins/security_solution/server/fixtures.ts b/x-pack/plugins/security_solution/server/fixtures.ts index cc8f7966cdd1f..0d8f91e481730 100644 --- a/x-pack/plugins/security_solution/server/fixtures.ts +++ b/x-pack/plugins/security_solution/server/fixtures.ts @@ -6,12 +6,14 @@ */ import { coreMock } from '../../../../src/core/server/mocks'; +import { createFleetRequestHandlerContextMock } from '../../fleet/server/mocks'; import { licensingMock } from '../../licensing/server/mocks'; function createCoreRequestHandlerContextMock() { return { core: coreMock.createRequestHandlerContext(), licensing: licensingMock.createRequestHandlerContext(), + fleet: createFleetRequestHandlerContextMock(), }; } diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts b/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts index f8e393fc3994f..8fcdbd7304656 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts @@ -13,7 +13,7 @@ import { } from 'src/core/server'; import { SearchRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { getTrustedAppsList } from '../../endpoint/routes/trusted_apps/service'; -import { AgentService, AgentPolicyServiceInterface } from '../../../../fleet/server'; +import { AgentClient, AgentPolicyServiceInterface } from '../../../../fleet/server'; import { ExceptionListClient } from '../../../../lists/server'; import { EndpointAppContextService } from '../../endpoint/endpoint_app_context_services'; import { TELEMETRY_MAX_BUFFER_SIZE } from './constants'; @@ -32,7 +32,7 @@ import { export class TelemetryReceiver { private readonly logger: Logger; - private agentService?: AgentService; + private agentClient?: AgentClient; private agentPolicyService?: AgentPolicyServiceInterface; private esClient?: ElasticsearchClient; private exceptionListClient?: ExceptionListClient; @@ -52,7 +52,7 @@ export class TelemetryReceiver { exceptionListClient?: ExceptionListClient ) { this.kibanaIndex = kibanaIndex; - this.agentService = endpointContextService?.getAgentService(); + this.agentClient = endpointContextService?.getAgentService()?.asInternalUser; this.agentPolicyService = endpointContextService?.getAgentPolicyService(); this.esClient = core?.elasticsearch.client.asInternalUser; this.exceptionListClient = exceptionListClient; @@ -70,7 +70,7 @@ export class TelemetryReceiver { throw Error('elasticsearch client is unavailable: cannot retrieve fleet policy responses'); } - return this.agentService?.listAgents(this.esClient, { + return this.agentClient?.listAgents({ perPage: this.max_records, showInactive: true, sortField: 'enrolled_at', diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 87f0ed7193a67..5e7bf0659947c 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -402,8 +402,6 @@ export class Plugin implements ISecuritySolutionPlugin { endpointMetadataService: new EndpointMetadataService( core.savedObjects, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - plugins.fleet?.agentService!, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion plugins.fleet?.agentPolicyService!, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion plugins.fleet?.packagePolicyService!, diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts index 918d3aadfd6e8..8bbd30a4a6a1d 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/helpers.ts @@ -9,6 +9,7 @@ import { set } from '@elastic/safer-lodash-set/fp'; import { get, has, head } from 'lodash/fp'; import { IScopedClusterClient, + KibanaRequest, SavedObjectsClientContract, } from '../../../../../../../../../src/core/server'; import { hostFieldsMap } from '../../../../../../common/ecs/ecs_fields'; @@ -180,35 +181,32 @@ export const getHostEndpoint = async ( esClient: IScopedClusterClient; savedObjectsClient: SavedObjectsClientContract; endpointContext: EndpointAppContext; + request: KibanaRequest; } ): Promise => { if (!id) { return null; } - const { esClient, endpointContext } = deps; + const { esClient, endpointContext, request } = deps; const logger = endpointContext.logFactory.get('metadata'); try { - const agentService = endpointContext.service.getAgentService(); + const fleetServices = endpointContext.service.getScopedFleetServices(request); + const endpointMetadataService = endpointContext.service.getEndpointMetadataService(); - if (!agentService) { - throw new Error('agentService not available'); - } - - const endpointData = await endpointContext.service - .getEndpointMetadataService() + const endpointData = await endpointMetadataService // Using `internalUser` ES client below due to the fact that Fleet data has been moved to // system indices (`.fleet*`). Because this is a readonly action, this should be ok to do // here until proper RBOC controls are implemented - .getEnrichedHostMetadata(esClient.asInternalUser, id); + .getEnrichedHostMetadata(esClient.asInternalUser, fleetServices, id); const fleetAgentId = endpointData.metadata.elastic.agent.id; const pendingActions = fleetAgentId ? getPendingActionCounts( esClient.asInternalUser, - endpointContext.service.getEndpointMetadataService(), + endpointMetadataService, [fleetAgentId], endpointContext.experimentalFeatures.pendingActionResponsesWithAck ) diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx index 7729934123899..89d691e5c9ce8 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx @@ -14,6 +14,7 @@ import { } from './__mocks__'; import { IScopedClusterClient, + KibanaRequest, SavedObjectsClientContract, } from '../../../../../../../../../src/core/server'; import { EndpointAppContext } from '../../../../../endpoint/types'; @@ -35,6 +36,7 @@ const mockDeps = { }, service: {} as EndpointAppContextService, } as EndpointAppContext, + request: {} as KibanaRequest, }; describe('hostDetails search strategy', () => { diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.ts index 7a4301185de4c..aedb1061af2fa 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.ts @@ -23,6 +23,7 @@ import { formatHostItem, getHostEndpoint } from './helpers'; import { EndpointAppContext } from '../../../../../endpoint/types'; import { IScopedClusterClient, + KibanaRequest, SavedObjectsClientContract, } from '../../../../../../../../../src/core/server'; @@ -35,6 +36,7 @@ export const hostDetails: SecuritySolutionFactory = { esClient: IScopedClusterClient; savedObjectsClient: SavedObjectsClientContract; endpointContext: EndpointAppContext; + request: KibanaRequest; } ): Promise => { const aggregations = get('aggregations', response.rawResponse); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/types.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/types.ts index 8fc1192fa95a6..4fe65b7e219f3 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/types.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/types.ts @@ -7,6 +7,7 @@ import type { IScopedClusterClient, + KibanaRequest, SavedObjectsClientContract, } from '../../../../../../../src/core/server'; import type { @@ -29,6 +30,7 @@ export interface SecuritySolutionFactory { esClient: IScopedClusterClient; savedObjectsClient: SavedObjectsClientContract; endpointContext: EndpointAppContext; + request: KibanaRequest; } ) => Promise>; } diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/index.ts index 0883a144615bc..040ade4dad41c 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/index.ts @@ -49,6 +49,7 @@ export const securitySolutionSearchStrategyProvider = ;