diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts index 53b677bb1389ea..13dcea75f31d0e 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts @@ -24,7 +24,7 @@ export const postAgentAcksHandlerBuilder = function( return async (context, request, response) => { try { const soClient = ackService.getSavedObjectsClientContract(request); - const res = APIKeyService.parseApiKey(request.headers); + const res = APIKeyService.parseApiKeyFromHeaders(request.headers); const agent = await ackService.getAgentByAccessAPIKeyId(soClient, res.apiKeyId as string); const agentEvents = request.body.events as AgentEvent[]; diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts index 7d991f5ad2cc25..adff1fda11200d 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts @@ -175,7 +175,7 @@ export const postAgentCheckinHandler: RequestHandler< > = async (context, request, response) => { try { const soClient = getInternalUserSOClient(request); - const res = APIKeyService.parseApiKey(request.headers); + const res = APIKeyService.parseApiKeyFromHeaders(request.headers); const agent = await AgentService.getAgentByAccessAPIKeyId(soClient, res.apiKeyId); const { actions } = await AgentService.agentCheckin( soClient, @@ -216,7 +216,7 @@ export const postAgentEnrollHandler: RequestHandler< > = async (context, request, response) => { try { const soClient = getInternalUserSOClient(request); - const { apiKeyId } = APIKeyService.parseApiKey(request.headers); + const { apiKeyId } = APIKeyService.parseApiKeyFromHeaders(request.headers); const enrollmentAPIKey = await APIKeyService.getEnrollmentAPIKeyById(soClient, apiKeyId); if (!enrollmentAPIKey || !enrollmentAPIKey.active) { diff --git a/x-pack/plugins/ingest_manager/server/services/agents/unenroll.ts b/x-pack/plugins/ingest_manager/server/services/agents/unenroll.ts index bf6f6526be0696..18af9fd4de73f2 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/unenroll.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/unenroll.ts @@ -7,6 +7,8 @@ import { SavedObjectsClientContract } from 'src/core/server'; import { AgentSOAttributes } from '../../types'; import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; +import { getAgent } from './crud'; +import * as APIKeyService from '../api_keys'; export async function unenrollAgents( soClient: SavedObjectsClientContract, @@ -15,9 +17,7 @@ export async function unenrollAgents( const response = []; for (const id of toUnenrollIds) { try { - await soClient.update(AGENT_SAVED_OBJECT_TYPE, id, { - active: false, - }); + await unenrollAgent(soClient, id); response.push({ id, success: true, @@ -33,3 +33,22 @@ export async function unenrollAgents( return response; } + +async function unenrollAgent(soClient: SavedObjectsClientContract, agentId: string) { + const agent = await getAgent(soClient, agentId); + + await Promise.all([ + agent.access_api_key_id + ? APIKeyService.invalidateAPIKey(soClient, agent.access_api_key_id) + : undefined, + agent.default_api_key + ? APIKeyService.invalidateAPIKey( + soClient, + APIKeyService.parseApiKey(agent.default_api_key).apiKeyId + ) + : undefined, + ]); + await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { + active: false, + }); +} diff --git a/x-pack/plugins/ingest_manager/server/services/api_keys/index.ts b/x-pack/plugins/ingest_manager/server/services/api_keys/index.ts index 7f3f9f5281f0cb..329945b669f8f3 100644 --- a/x-pack/plugins/ingest_manager/server/services/api_keys/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/api_keys/index.ts @@ -9,6 +9,7 @@ import { ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE } from '../../constants'; import { EnrollmentAPIKeySOAttributes, EnrollmentAPIKey } from '../../types'; import { createAPIKey } from './security'; +export { invalidateAPIKey } from './security'; export * from './enrollment_api_key'; export async function generateOutputApiKey( @@ -77,7 +78,7 @@ export async function getEnrollmentAPIKeyById( return enrollmentAPIKey; } -export function parseApiKey(headers: KibanaRequest['headers']) { +export function parseApiKeyFromHeaders(headers: KibanaRequest['headers']) { const authorizationHeader = headers.authorization; if (!authorizationHeader) { @@ -93,9 +94,11 @@ export function parseApiKey(headers: KibanaRequest['headers']) { } const apiKey = authorizationHeader.split(' ')[1]; - if (!apiKey) { - throw new Error('Authorization header is malformed'); - } + + return parseApiKey(apiKey); +} + +export function parseApiKey(apiKey: string) { const apiKeyId = Buffer.from(apiKey, 'base64') .toString('utf8') .split(':')[0]; diff --git a/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts b/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts index 4b6b28e3d6350f..b484f1f5a8ed2c 100644 --- a/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts +++ b/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts @@ -5,17 +5,58 @@ */ import expect from '@kbn/expect'; +import uuid from 'uuid'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { setupIngest } from './agents/services'; -export default function({ getService }: FtrProviderContext) { +export default function(providerContext: FtrProviderContext) { + const { getService } = providerContext; const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); + const esClient = getService('es'); describe('fleet_unenroll_agent', () => { + let accessAPIKeyId: string; + let outputAPIKeyId: string; before(async () => { await esArchiver.loadIfNeeded('fleet/agents'); }); + setupIngest(providerContext); + beforeEach(async () => { + const { body: accessAPIKeyBody } = await esClient.security.createApiKey({ + body: { + name: `test access api key: ${uuid.v4()}`, + }, + }); + accessAPIKeyId = accessAPIKeyBody.id; + const { body: outputAPIKeyBody } = await esClient.security.createApiKey({ + body: { + name: `test output api key: ${uuid.v4()}`, + }, + }); + outputAPIKeyId = outputAPIKeyBody.id; + const { + body: { _source: agentDoc }, + } = await esClient.get({ + index: '.kibana', + id: 'agents:agent1', + }); + // @ts-ignore + agentDoc.agents.access_api_key_id = accessAPIKeyId; + agentDoc.agents.default_api_key = Buffer.from( + `${outputAPIKeyBody.id}:${outputAPIKeyBody.api_key}` + ).toString('base64'); + + await esClient.update({ + index: '.kibana', + id: 'agents:agent1', + refresh: 'true', + body: { + doc: agentDoc, + }, + }); + }); after(async () => { await esArchiver.unload('fleet/agents'); }); @@ -54,6 +95,31 @@ export default function({ getService }: FtrProviderContext) { expect(body.results[0].success).to.be(true); }); + it('should invalidate related API keys', async () => { + const { body } = await supertest + .post(`/api/ingest_manager/fleet/agents/unenroll`) + .set('kbn-xsrf', 'xxx') + .send({ + ids: ['agent1'], + }) + .expect(200); + + expect(body).to.have.keys('results', 'success'); + expect(body.success).to.be(true); + + const { + body: { api_keys: accessAPIKeys }, + } = await esClient.security.getApiKey({ id: accessAPIKeyId }); + expect(accessAPIKeys).length(1); + expect(accessAPIKeys[0].invalidated).eql(true); + + const { + body: { api_keys: outputAPIKeys }, + } = await esClient.security.getApiKey({ id: outputAPIKeyId }); + expect(outputAPIKeys).length(1); + expect(outputAPIKeys[0].invalidated).eql(true); + }); + it('allow to unenroll using a kibana query', async () => { const { body } = await supertest .post(`/api/ingest_manager/fleet/agents/unenroll`)