Skip to content

Commit

Permalink
fix for force unenroll
Browse files Browse the repository at this point in the history
  • Loading branch information
juliaElastic committed Sep 21, 2022
1 parent bbca768 commit 278e6df
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 30 deletions.
8 changes: 7 additions & 1 deletion x-pack/plugins/fleet/common/types/models/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,13 @@ export type AgentStatus =

export type SimplifiedAgentStatus = 'healthy' | 'unhealthy' | 'updating' | 'offline' | 'inactive';

export type AgentActionType = 'UNENROLL' | 'UPGRADE' | 'SETTINGS' | 'POLICY_REASSIGN' | 'CANCEL';
export type AgentActionType =
| 'UNENROLL'
| 'UPGRADE'
| 'SETTINGS'
| 'POLICY_REASSIGN'
| 'CANCEL'
| 'FORCE_UNENROLL';

type FleetServerAgentComponentStatusTuple = typeof FleetServerAgentComponentStatuses;
export type FleetServerAgentComponentStatus = FleetServerAgentComponentStatusTuple[number];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,11 @@ const actionNames: {
completedText: 'unenrolled',
cancelledText: 'unenrollment',
},
FORCE_UNENROLL: {
inProgressText: 'Force unenrolling',
completedText: 'force unenrolled',
cancelledText: 'force unenrollment',
},
CANCEL: { inProgressText: 'Cancelling', completedText: 'cancelled', cancelledText: '' },
ACTION: { inProgressText: 'Actioning', completedText: 'actioned', cancelledText: 'action' },
};
Expand Down
11 changes: 2 additions & 9 deletions x-pack/plugins/fleet/server/services/agents/action_status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export async function getActionStatuses(
size: 0,
aggs: {
ack_counts: {
terms: { field: 'action_id' },
terms: { field: 'action_id', size: actions.length },
aggs: {
max_timestamp: { max: { field: '@timestamp' } },
},
Expand All @@ -61,7 +61,7 @@ export async function getActionStatuses(
const nbAgentsAck = matchingBucket?.doc_count ?? 0;
const completionTime = (matchingBucket?.max_timestamp as any)?.value_as_string;
const nbAgentsActioned = action.nbAgentsActioned || action.nbAgentsActionCreated;
const complete = nbAgentsAck === nbAgentsActioned;
const complete = nbAgentsAck >= nbAgentsActioned;
const cancelledAction = cancelledActions.find((a) => a.actionId === action.actionId);

let errorCount = 0;
Expand Down Expand Up @@ -161,13 +161,6 @@ async function _getActions(
},
},
],
must: [
{
exists: {
field: 'agents',
},
},
],
},
},
body: {
Expand Down
38 changes: 37 additions & 1 deletion x-pack/plugins/fleet/server/services/agents/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export async function bulkCreateAgentActionResults(
results: Array<{
actionId: string;
agentId: string;
error: string;
error?: string;
}>
): Promise<void> {
if (results.length === 0) {
Expand Down Expand Up @@ -164,6 +164,41 @@ export async function getAgentActions(esClient: ElasticsearchClient, actionId: s
}));
}

export async function getUnenrollAgentActions(
esClient: ElasticsearchClient
): Promise<FleetServerAgentAction[]> {
const res = await esClient.search<FleetServerAgentAction>({
index: AGENT_ACTIONS_INDEX,
query: {
bool: {
must: [
{
term: {
type: 'UNENROLL',
},
},
{
exists: {
field: 'agents',
},
},
{
range: {
expiration: { gte: new Date().toISOString() },
},
},
],
},
},
size: SO_SEARCH_LIMIT,
});

return res.hits.hits.map((hit) => ({
...hit._source,
id: hit._id,
}));
}

export async function cancelAgentAction(esClient: ElasticsearchClient, actionId: string) {
const res = await esClient.search<FleetServerAgentAction>({
index: AGENT_ACTIONS_INDEX,
Expand Down Expand Up @@ -233,4 +268,5 @@ export interface ActionsService {
) => Promise<AgentAction>;

getAgentActions: (esClient: ElasticsearchClient, actionId: string) => Promise<any[]>;
getUnenrollAgentActions: (esClient: ElasticsearchClient) => Promise<any[]>;
}
7 changes: 5 additions & 2 deletions x-pack/plugins/fleet/server/services/agents/unenroll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server';

import uuid from 'uuid';

import type { Agent, BulkActionResult } from '../../types';
import { HostedAgentPolicyRestrictionRelatedError } from '../../errors';
import { SO_SEARCH_LIMIT } from '../../constants';
Expand All @@ -20,6 +22,7 @@ import {
invalidateAPIKeysForAgents,
UnenrollActionRunner,
unenrollBatch,
updateActionsForForceUnenroll,
} from './unenroll_action_runner';

async function unenrollAgentIsAllowed(
Expand Down Expand Up @@ -50,7 +53,7 @@ export async function unenrollAgent(
await unenrollAgentIsAllowed(soClient, esClient, agentId);
}
if (options?.revoke) {
return forceUnenrollAgent(soClient, esClient, agentId);
return forceUnenrollAgent(esClient, agentId);
}
const now = new Date().toISOString();
await createAgentAction(esClient, {
Expand Down Expand Up @@ -102,7 +105,6 @@ export async function unenrollAgents(
}

export async function forceUnenrollAgent(
soClient: SavedObjectsClientContract,
esClient: ElasticsearchClient,
agentIdOrAgent: string | Agent
) {
Expand All @@ -116,4 +118,5 @@ export async function forceUnenrollAgent(
active: false,
unenrolled_at: new Date().toISOString(),
});
await updateActionsForForceUnenroll(esClient, [agent.id], uuid(), 1);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
import uuid from 'uuid';
import type { SavedObjectsClientContract, ElasticsearchClient } from '@kbn/core/server';

import { intersection } from 'lodash';

import type { Agent, BulkActionResult } from '../../types';

import { HostedAgentPolicyRestrictionRelatedError } from '../../errors';
import { FleetError, HostedAgentPolicyRestrictionRelatedError } from '../../errors';

import { invalidateAPIKeys } from '../api_keys';

Expand All @@ -18,7 +20,11 @@ import { appContextService } from '../app_context';
import { ActionRunner } from './action_runner';

import { errorsToResults, bulkUpdateAgents } from './crud';
import { bulkCreateAgentActionResults, createAgentAction } from './actions';
import {
bulkCreateAgentActionResults,
createAgentAction,
getUnenrollAgentActions,
} from './actions';
import { getHostedPolicies, isHostedAgent } from './hosted_agent';
import { BulkActionTaskType } from './bulk_actions_resolver';

Expand Down Expand Up @@ -48,23 +54,19 @@ export async function unenrollBatch(
},
skipSuccess?: boolean
): Promise<{ items: BulkActionResult[] }> {
// Filter to those not already unenrolled, or unenrolling
const agentsEnrolled = givenAgents.filter((agent) => {
if (options.revoke) {
return !agent.unenrolled_at;
}
return !agent.unenrollment_started_at && !agent.unenrolled_at;
});

const hostedPolicies = await getHostedPolicies(soClient, agentsEnrolled);

const hostedPolicies = await getHostedPolicies(soClient, givenAgents);
const outgoingErrors: Record<Agent['id'], Error> = {};

// And which are allowed to unenroll
const agentsToUpdate = options.force
? agentsEnrolled
: agentsEnrolled.reduce<Agent[]>((agents, agent) => {
if (isHostedAgent(hostedPolicies, agent)) {
? givenAgents
: givenAgents.reduce<Agent[]>((agents, agent) => {
if (
(options.revoke && agent.unenrolled_at) ||
(!options.revoke && (agent.unenrollment_started_at || agent.unenrolled_at))
) {
outgoingErrors[agent.id] = new FleetError(`Agent ${agent.id} already unenrolled`);
} else if (isHostedAgent(hostedPolicies, agent)) {
outgoingErrors[agent.id] = new HostedAgentPolicyRestrictionRelatedError(
`Cannot unenroll ${agent.id} from a hosted agent policy ${agent.policy_id}`
);
Expand All @@ -76,17 +78,21 @@ export async function unenrollBatch(

const actionId = options.actionId ?? uuid();
const errorCount = Object.keys(outgoingErrors).length;
const total = options.total ?? agentsToUpdate.length + errorCount;
const total = options.total ?? givenAgents.length;

const agentIds = agentsToUpdate.map((agent) => agent.id);

const now = new Date().toISOString();
if (options.revoke) {
// Get all API keys that need to be invalidated
await invalidateAPIKeysForAgents(agentsToUpdate);

await updateActionsForForceUnenroll(esClient, agentIds, actionId, total);
} else {
// Create unenroll action for each agent
await createAgentAction(esClient, {
id: actionId,
agents: agentsToUpdate.map((agent) => agent.id),
agents: agentIds,
created_at: now,
type: 'UNENROLL',
total,
Expand Down Expand Up @@ -126,6 +132,44 @@ export async function unenrollBatch(
};
}

export async function updateActionsForForceUnenroll(
esClient: ElasticsearchClient,
agentIds: string[],
actionId: string,
total: number
) {
// creating an unenroll so that force unenroll shows up in activity
await createAgentAction(esClient, {
id: actionId,
agents: [],
created_at: new Date().toISOString(),
type: 'FORCE_UNENROLL',
total,
});
await bulkCreateAgentActionResults(
esClient,
agentIds.map((agentId) => ({
agentId,
actionId,
}))
);

// updating action results for those agents that are there in a pending unenroll action
const unenrollActions = await getUnenrollAgentActions(esClient);
for (const action of unenrollActions) {
const commonAgents = intersection(action.agents, agentIds);
if (commonAgents.length > 0) {
await bulkCreateAgentActionResults(
esClient,
commonAgents.map((agentId) => ({
agentId,
actionId: action.action_id!,
}))
);
}
}
}

export async function invalidateAPIKeysForAgents(agents: Agent[]) {
const apiKeys = agents.reduce<string[]>((keys, agent) => {
if (agent.access_api_key_id) {
Expand Down

0 comments on commit 278e6df

Please sign in to comment.