[Fleet] Use fleet server indices for enrollment keys and to list agents with a feature flag (#86179)

This commit is contained in:
Nicolas Chaulet 2021-01-20 19:29:04 -05:00 committed by GitHub
parent 25f16db4d9
commit 3b728b73cf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
71 changed files with 1528 additions and 406 deletions

View file

@ -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';

View file

@ -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',

View file

@ -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';

View file

@ -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() {

View file

@ -11,6 +11,7 @@ export interface FleetConfigType {
registryUrl?: string;
registryProxyUrl?: string;
agents: {
fleetServerEnabled: boolean;
enabled: boolean;
tlsCheckDisabled: boolean;
pollingRequestTimeout: number;

View file

@ -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;
}

View file

@ -15,3 +15,31 @@ export interface EnrollmentAPIKey {
}
export type EnrollmentAPIKeySOAttributes = Omit<EnrollmentAPIKey, 'id'>;
// 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;
}

View file

@ -13,6 +13,7 @@ export const createConfigurationMock = (): FleetConfigType => {
registryProxyUrl: '',
agents: {
enabled: true,
fleetServerEnabled: false,
tlsCheckDisabled: true,
pollingRequestTimeout: 1000,
maxConcurrentConnections: 100,

View file

@ -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<AgentUsage> => {
export const getAgentUsage = async (
soClient?: SavedObjectsClient,
esClient?: ElasticsearchClient
): Promise<AgentUsage> => {
// 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<Agen
};
}
const { total, online, error, offline } = await AgentService.getAgentStatusForAgentPolicy(
soClient
soClient,
esClient
);
return {
total,

View file

@ -5,11 +5,14 @@
*/
import { CoreSetup } from 'kibana/server';
import { SavedObjectsClient } from '../../../../../src/core/server';
import { ElasticsearchClient, SavedObjectsClient } from '../../../../../src/core/server';
export async function getInternalSavedObjectsClient(core: CoreSetup) {
export async function getInternalClients(
core: CoreSetup
): Promise<[SavedObjectsClient, ElasticsearchClient]> {
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];
});
}

View file

@ -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),
};
},

View file

@ -47,4 +47,7 @@ export {
// Defaults
DEFAULT_AGENT_POLICY,
DEFAULT_OUTPUT,
// Fleet Server index
ENROLLMENT_API_KEYS_INDEX,
AGENTS_INDEX,
} from '../../common';

View file

@ -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,

View file

@ -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(),

View file

@ -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<FleetStartContract> {
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: {

View file

@ -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<KibanaResponseFactory>;
let mockSavedObjectsClient: jest.Mocked<SavedObjectsClientContract>;
let mockElasticsearchClient: jest.Mocked<ElasticsearchClient>;
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<AcksService>;

View file

@ -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);

View file

@ -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<KibanaResponseFactory>;
let mockSavedObjectsClient: jest.Mocked<SavedObjectsClientContract>;
let mockElasticsearchClient: jest.Mocked<ElasticsearchClient>;
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,

View file

@ -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;

View file

@ -39,8 +39,10 @@ export const getAgentHandler: RequestHandler<
TypeOf<typeof GetOneAgentRequestSchema.params>
> = 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<typeof DeleteAgentRequestSchema.params>
> = 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<typeof UpdateAgentRequestSchema.body>
> = 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<typeof GetAgentsRequestSchema.query>
> = 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<typeof PutAgentReassignRequestSchema.body>
> = 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<typeof GetAgentStatusRequestSchema.query>
> = 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
);

View file

@ -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,
})
);

View file

@ -18,9 +18,10 @@ export const postAgentUnenrollHandler: RequestHandler<
TypeOf<typeof PostAgentUnenrollRequestSchema.body>
> = 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 = {};

View file

@ -83,6 +83,7 @@ export const postBulkAgentsUpgradeHandler: RequestHandler<
TypeOf<typeof PostBulkAgentUpgradeRequestSchema.body>
> = 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,

View file

@ -39,6 +39,7 @@ export const getAgentPoliciesHandler: RequestHandler<
TypeOf<typeof GetAgentPoliciesRequestSchema.query>
> = 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<typeof CreateAgentPolicyRequestSchema.body>
> = 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<typeof UpdateAgentPolicyRequestSchema.body>
> = 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<typeof CopyAgentPolicyRequestSchema.body>
> = 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<typeof DeleteAgentPolicyRequestSchema.body>
> = 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({

View file

@ -26,12 +26,18 @@ export const getEnrollmentApiKeysHandler: RequestHandler<
TypeOf<typeof GetEnrollmentAPIKeysRequestSchema.query>
> = 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<typeof PostEnrollmentAPIKeyRequestSchema.body>
> = 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<typeof DeleteEnrollmentAPIKeyRequestSchema.params>
> = 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<typeof GetOneEnrollmentAPIKeyRequestSchema.params>
> = 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 });

View file

@ -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,

View file

@ -74,6 +74,7 @@ export const createPackagePolicyHandler: RequestHandler<
TypeOf<typeof CreatePackagePolicyRequestSchema.body>
> = 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<typeof UpdatePackagePolicyRequestSchema.body>
> = 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<typeof DeletePackagePoliciesRequestSchema.body>
> = 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 }
);

View file

@ -36,10 +36,12 @@ export const putSettingsHandler: RequestHandler<
TypeOf<typeof PutSettingsRequestSchema.body>
> = 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 = {

View file

@ -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,
});

View file

@ -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);
});

View file

@ -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<AgentPolicySOAttributes>,
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<AgentPolicy> {
@ -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<AgentPolicy>,
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<AgentPolicy, 'name' | 'description'>,
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<AgentPolicy> {
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<Promise<SavedObjectsBulkUpdateResponse<AgentPolicy>>> {
const currentPolicies = await soClient.find<AgentPolicySOAttributes>({
@ -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<DeleteAgentPolicyResponse> {
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,

View file

@ -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);
}
}

View file

@ -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,

View file

@ -5,6 +5,7 @@
*/
import {
ElasticsearchClient,
KibanaRequest,
SavedObjectsBulkCreateObject,
SavedObjectsBulkResponse,
@ -40,6 +41,7 @@ const actionCache = new LRU<string, AgentAction>({
export async function acknowledgeAgentActions(
soClient: SavedObjectsClientContract,
esClient: ElasticsearchClient,
agent: Agent,
agentEvents: AgentEvent[]
): Promise<AgentAction[]> {
@ -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<AgentAction[]>;
@ -207,6 +210,8 @@ export interface AcksService {
getSavedObjectsClientContract: (kibanaRequest: KibanaRequest) => SavedObjectsClientContract;
getElasticsearchClientContract: () => ElasticsearchClient;
saveAgentEvents: (
soClient: SavedObjectsClientContract,
events: AgentEvent[]

View file

@ -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<Agent>;
getAgent: (
soClient: SavedObjectsClientContract,
esClient: ElasticsearchClient,
agentId: string
) => Promise<Agent>;
createAgentAction: (
soClient: SavedObjectsClientContract,

View file

@ -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 };
}

View file

@ -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,

View file

@ -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<AgentAction[]> {
@ -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');

View file

@ -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<AgentSOAttributes>({
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<ListWithKuery, 'page' | 'perPage'> & {
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<AgentSOAttributes>(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<ListWithKuery, 'kuery'>
): Promise<number> {
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<AgentSOAttributes>({
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<AgentSOAttributes>(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<AgentEventSOAttributes>({
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<AgentSOAttributes>(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);
}

View file

@ -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<ListWithKuery, 'page' | 'perPage'> & {
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<ListWithKuery, 'kuery'>
): Promise<number> {
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<ESSearchHit<AgentSOAttributes>>({
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<AgentSOAttributes>(
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<Agent> {
const response = await soClient.find<AgentSOAttributes>({
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<AgentSOAttributes>(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,
},
});
}

View file

@ -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<AgentSOAttributes>({
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<ListWithKuery, 'page' | 'perPage'> & {
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<AgentSOAttributes>(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<ListWithKuery, 'kuery'>
): Promise<number> {
const { kuery } = options;
const filters = [INACTIVE_AGENT_CONDITION];
if (kuery && kuery !== '') {
filters.push(normalizeKuery(AGENT_SAVED_OBJECT_TYPE, kuery));
}
const { total } = await soClient.find<AgentSOAttributes>({
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<AgentSOAttributes>(AGENT_SAVED_OBJECT_TYPE, agentId)
);
return agent;
}
export async function getAgents(soClient: SavedObjectsClientContract, agentIds: string[]) {
const agentSOs = await soClient.bulkGet<AgentSOAttributes>(
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<Agent> {
const response = await soClient.find<AgentSOAttributes>({
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<AgentSOAttributes>(AGENT_SAVED_OBJECT_TYPE, agentId, {
user_provided_metadata: data.userProvidedMetatada,
});
}
export async function deleteAgent(soClient: SavedObjectsClientContract, agentId: string) {
await soClient.update<AgentSOAttributes>(AGENT_SAVED_OBJECT_TYPE, agentId, {
active: false,
});
}

View file

@ -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<AgentSOAttributes>): 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 ?? [],
};
}

View file

@ -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,
})

View file

@ -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<AgentSOAttributes>);
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<AgentSOAttributes>);
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<AgentSOAttributes>);
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<AgentSOAttributes>);
const status = await getAgentStatusById(mockSavedObjectsClient, 'id');
const status = await getAgentStatusById(mockSavedObjectsClient, mockElasticsearchClient, 'id');
expect(status).toEqual('unenrolling');
});
});

View file

@ -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<AgentStatus> {
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<string | undefined>) {
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,

View file

@ -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,
})

View file

@ -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,

View file

@ -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,
})

View file

@ -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<EnrollmentAPIKeySOAttributes>({
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<EnrollmentAPIKeySOAttributes>(
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<EnrollmentAPIKeySOAttributes>(
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<EnrollmentAPIKeySOAttributes>): EnrollmentAPIKey {
if (error) {
throw new Error(error.message);
}
return {
id,
...attributes,
};
}

View file

@ -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<T> {
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<T> = SearchResponse<T>['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<SearchResponse<FleetServerEnrollmentAPIKey>>({
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<EnrollmentAPIKey> {
try {
const res = await esClient.get<SearchHit<FleetServerEnrollmentAPIKey>>({
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<EnrollmentAPIKey> {
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<FleetServerEnrollmentAPIKey>): EnrollmentAPIKey {
return {
id: doc._id,
...doc._source,
created_at: doc._source.created_at as string,
active: doc._source.active || false,
};
}

View file

@ -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<EnrollmentAPIKeySOAttributes>({
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<EnrollmentAPIKeySOAttributes>(
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<EnrollmentAPIKeySOAttributes>(
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<EnrollmentAPIKeySOAttributes>): EnrollmentAPIKey {
if (error) {
throw new Error(error.message);
}
return {
id,
...attributes,
};
}

View file

@ -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<FleetConfigType>;
private configSubject$?: BehaviorSubject<FleetConfigType>;
@ -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;
}

View file

@ -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);
}
}
}

View file

@ -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<AgentStatus>;
getAgentStatusById(
soClient: SavedObjectsClientContract,
esClient: ElasticsearchClient,
agentId: string
): Promise<AgentStatus>;
/**
* List agents
*/

View file

@ -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()
)

View file

@ -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<DeletePackagePoliciesResponse> {
@ -331,6 +347,7 @@ class PackagePolicyService {
if (!options?.skipUnassignFromAgentPolicies) {
await agentPolicyService.unassignPackagePolicies(
soClient,
esClient,
packagePolicy.policy_id,
[packagePolicy.id],
{

View file

@ -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);
});

View file

@ -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<SetupStatus> {
return awaitIfPending(async () => createSetupSideEffects(soClient, callCluster));
return awaitIfPending(async () => createSetupSideEffects(soClient, esClient, callCluster));
}
async function createSetupSideEffects(
soClient: SavedObjectsClientContract,
esClient: ElasticsearchClient,
callCluster: CallESAsCurrentUser
): Promise<SetupStatus> {
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,
});
}

View file

@ -77,6 +77,8 @@ export {
PostAgentCheckinRequest,
DataType,
dataTypes,
// Fleet Server types
FleetServerEnrollmentAPIKey,
} from '../../common';
export type CallESAsCurrentUser = LegacyScopedClusterClient['callAsCurrentUser'];

View file

@ -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<PackagePolicyServiceInterface>;
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<ILicense> = 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);
});

View file

@ -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}`

View file

@ -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

View file

@ -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<SavedObjectsClientContract>;
let mockElasticsearchClient: jest.Mocked<ElasticsearchClient>;
let mockAgentService: jest.Mocked<AgentService>;
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();

View file

@ -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<string[]> {
@ -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;
}

View file

@ -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<SavedObjectsClientContract>;
let mockElasticsearchClient: jest.Mocked<ElasticsearchClient>;
let mockAgentService: jest.Mocked<AgentService>;
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']);
});

View file

@ -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<string[]> {
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;
}

View file

@ -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
);

View file

@ -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<string, number>): { [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<Map<string, number>> {
@ -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)) {

View file

@ -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',

View file

@ -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()}`
);

View file

@ -355,6 +355,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
this.policyWatcher = new PolicyWatcher(
plugins.fleet!.packagePolicyService,
core.savedObjects,
core.elasticsearch,
this.logger
);
this.policyWatcher.start(licenseService);