Skip to content

Commit

Permalink
EMT-248: implement ack resource to accept event payload to acknowledg…
Browse files Browse the repository at this point in the history
…e agent actions (#60218) (#60324)

[Ingest]EMT-248: implement ack resource to accept event payload to acknowledge agent actions
  • Loading branch information
nnamdifrankie committed Mar 17, 2020
1 parent 4827287 commit 1c968e1
Show file tree
Hide file tree
Showing 12 changed files with 539 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,18 @@ export interface PostAgentEnrollResponse {

export interface PostAgentAcksRequest {
body: {
action_ids: string[];
events: AgentEvent[];
};
params: {
agentId: string;
};
}

export interface PostAgentAcksResponse {
action: string;
success: boolean;
}

export interface PostAgentUnenrollRequest {
body: { kuery: string } | { ids: string[] };
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* 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 { postAgentAcksHandlerBuilder } from './acks_handlers';
import {
KibanaResponseFactory,
RequestHandlerContext,
SavedObjectsClientContract,
} from 'kibana/server';
import { httpServerMock, savedObjectsClientMock } from '../../../../../../src/core/server/mocks';
import { PostAgentAcksResponse } from '../../../common/types/rest_spec';
import { AckEventSchema } from '../../types/models';
import { AcksService } from '../../services/agents';

describe('test acks schema', () => {
it('validate that ack event schema expect action id', async () => {
expect(() =>
AckEventSchema.validate({
type: 'ACTION_RESULT',
subtype: 'CONFIG',
timestamp: '2019-01-04T14:32:03.36764-05:00',
agent_id: 'agent',
message: 'hello',
payload: 'payload',
})
).toThrow(Error);

expect(
AckEventSchema.validate({
type: 'ACTION_RESULT',
subtype: 'CONFIG',
timestamp: '2019-01-04T14:32:03.36764-05:00',
agent_id: 'agent',
action_id: 'actionId',
message: 'hello',
payload: 'payload',
})
).toBeTruthy();
});
});

describe('test acks handlers', () => {
let mockResponse: jest.Mocked<KibanaResponseFactory>;
let mockSavedObjectsClient: jest.Mocked<SavedObjectsClientContract>;

beforeEach(() => {
mockSavedObjectsClient = savedObjectsClientMock.create();
mockResponse = httpServerMock.createResponseFactory();
});

it('should succeed on valid agent event', async () => {
const mockRequest = httpServerMock.createKibanaRequest({
headers: {
authorization: 'ApiKey TmVqTDBIQUJsRkw1em52R1ZIUF86NS1NaTItdHFUTHFHbThmQW1Fb0ljUQ==',
},
body: {
events: [
{
type: 'ACTION_RESULT',
subtype: 'CONFIG',
timestamp: '2019-01-04T14:32:03.36764-05:00',
action_id: 'action1',
agent_id: 'agent',
message: 'message',
},
],
},
});

const ackService: AcksService = {
acknowledgeAgentActions: jest.fn().mockReturnValueOnce([
{
type: 'CONFIG_CHANGE',
id: 'action1',
},
]),
getAgentByAccessAPIKeyId: jest.fn().mockReturnValueOnce({
id: 'agent',
}),
getSavedObjectsClientContract: jest.fn().mockReturnValueOnce(mockSavedObjectsClient),
saveAgentEvents: jest.fn(),
} as jest.Mocked<AcksService>;

const postAgentAcksHandler = postAgentAcksHandlerBuilder(ackService);
await postAgentAcksHandler(({} as unknown) as RequestHandlerContext, mockRequest, mockResponse);
expect(mockResponse.ok.mock.calls[0][0]?.body as PostAgentAcksResponse).toEqual({
action: 'acks',
success: true,
});
});
});
69 changes: 69 additions & 0 deletions x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* 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.
*/

// handlers that handle events from agents in response to actions received

import { RequestHandler } from 'kibana/server';
import { TypeOf } from '@kbn/config-schema';
import { PostAgentAcksRequestSchema } from '../../types/rest_spec';
import * as APIKeyService from '../../services/api_keys';
import { AcksService } from '../../services/agents';
import { AgentEvent } from '../../../common/types/models';
import { PostAgentAcksResponse } from '../../../common/types/rest_spec';

export const postAgentAcksHandlerBuilder = function(
ackService: AcksService
): RequestHandler<
TypeOf<typeof PostAgentAcksRequestSchema.params>,
undefined,
TypeOf<typeof PostAgentAcksRequestSchema.body>
> {
return async (context, request, response) => {
try {
const soClient = ackService.getSavedObjectsClientContract(request);
const res = APIKeyService.parseApiKey(request.headers);
const agent = await ackService.getAgentByAccessAPIKeyId(soClient, res.apiKeyId as string);
const agentEvents = request.body.events as AgentEvent[];

// validate that all events are for the authorized agent obtained from the api key
const notAuthorizedAgentEvent = agentEvents.filter(
agentEvent => agentEvent.agent_id !== agent.id
);

if (notAuthorizedAgentEvent && notAuthorizedAgentEvent.length > 0) {
return response.badRequest({
body:
'agent events contains events with different agent id from currently authorized agent',
});
}

const agentActions = await ackService.acknowledgeAgentActions(soClient, agent, agentEvents);

if (agentActions.length > 0) {
await ackService.saveAgentEvents(soClient, agentEvents);
}

const body: PostAgentAcksResponse = {
action: 'acks',
success: true,
};

return response.ok({ body });
} catch (e) {
if (e.isBoom) {
return response.customError({
statusCode: e.output.statusCode,
body: { message: e.message },
});
}

return response.customError({
statusCode: 500,
body: { message: e.message },
});
}
};
};
36 changes: 1 addition & 35 deletions x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,14 @@ import {
GetOneAgentEventsRequestSchema,
PostAgentCheckinRequestSchema,
PostAgentEnrollRequestSchema,
PostAgentAcksRequestSchema,
PostAgentUnenrollRequestSchema,
GetAgentStatusRequestSchema,
} from '../../types';
import * as AgentService from '../../services/agents';
import * as APIKeyService from '../../services/api_keys';
import { appContextService } from '../../services/app_context';

function getInternalUserSOClient(request: KibanaRequest) {
export function getInternalUserSOClient(request: KibanaRequest) {
// soClient as kibana internal users, be carefull on how you use it, security is not enabled
return appContextService.getSavedObjects().getScopedClient(request, {
excludedWrappers: ['security'],
Expand Down Expand Up @@ -210,39 +209,6 @@ export const postAgentCheckinHandler: RequestHandler<
}
};

export const postAgentAcksHandler: RequestHandler<
TypeOf<typeof PostAgentAcksRequestSchema.params>,
undefined,
TypeOf<typeof PostAgentAcksRequestSchema.body>
> = async (context, request, response) => {
try {
const soClient = getInternalUserSOClient(request);
const res = APIKeyService.parseApiKey(request.headers);
const agent = await AgentService.getAgentByAccessAPIKeyId(soClient, res.apiKeyId as string);

await AgentService.acknowledgeAgentActions(soClient, agent, request.body.action_ids);

const body = {
action: 'acks',
success: true,
};

return response.ok({ body });
} catch (e) {
if (e.isBoom) {
return response.customError({
statusCode: e.output.statusCode,
body: { message: e.message },
});
}

return response.customError({
statusCode: 500,
body: { message: e.message },
});
}
};

export const postAgentEnrollHandler: RequestHandler<
undefined,
undefined,
Expand Down
11 changes: 9 additions & 2 deletions x-pack/plugins/ingest_manager/server/routes/agent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,12 @@ import {
getAgentEventsHandler,
postAgentCheckinHandler,
postAgentEnrollHandler,
postAgentAcksHandler,
postAgentsUnenrollHandler,
getAgentStatusForConfigHandler,
getInternalUserSOClient,
} from './handlers';
import { postAgentAcksHandlerBuilder } from './acks_handlers';
import * as AgentService from '../../services/agents';

export const registerRoutes = (router: IRouter) => {
// Get one
Expand Down Expand Up @@ -101,7 +103,12 @@ export const registerRoutes = (router: IRouter) => {
validate: PostAgentAcksRequestSchema,
options: { tags: [] },
},
postAgentAcksHandler
postAgentAcksHandlerBuilder({
acknowledgeAgentActions: AgentService.acknowledgeAgentActions,
getAgentByAccessAPIKeyId: AgentService.getAgentByAccessAPIKeyId,
getSavedObjectsClientContract: getInternalUserSOClient,
saveAgentEvents: AgentService.saveAgentEvents,
})
);

router.post(
Expand Down
118 changes: 118 additions & 0 deletions x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* 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 { savedObjectsClientMock } from '../../../../../../src/core/server/saved_objects/service/saved_objects_client.mock';
import { Agent, AgentAction, AgentEvent } from '../../../common/types/models';
import { AGENT_TYPE_PERMANENT } from '../../../common/constants';
import { acknowledgeAgentActions } from './acks';
import { isBoom } from 'boom';

describe('test agent acks services', () => {
it('should succeed on valid and matched actions', async () => {
const mockSavedObjectsClient = savedObjectsClientMock.create();
const agentActions = await acknowledgeAgentActions(
mockSavedObjectsClient,
({
id: 'id',
type: AGENT_TYPE_PERMANENT,
actions: [
{
type: 'CONFIG_CHANGE',
id: 'action1',
sent_at: '2020-03-14T19:45:02.620Z',
timestamp: '2019-01-04T14:32:03.36764-05:00',
created_at: '2020-03-14T19:45:02.620Z',
},
],
} as unknown) as Agent,
[
{
type: 'ACTION_RESULT',
subtype: 'CONFIG',
timestamp: '2019-01-04T14:32:03.36764-05:00',
action_id: 'action1',
agent_id: 'id',
} as AgentEvent,
]
);
expect(agentActions).toEqual([
({
type: 'CONFIG_CHANGE',
id: 'action1',
sent_at: '2020-03-14T19:45:02.620Z',
timestamp: '2019-01-04T14:32:03.36764-05:00',
created_at: '2020-03-14T19:45:02.620Z',
} as unknown) as AgentAction,
]);
});

it('should fail for actions that cannot be found on agent actions list', async () => {
const mockSavedObjectsClient = savedObjectsClientMock.create();
try {
await acknowledgeAgentActions(
mockSavedObjectsClient,
({
id: 'id',
type: AGENT_TYPE_PERMANENT,
actions: [
{
type: 'CONFIG_CHANGE',
id: 'action1',
sent_at: '2020-03-14T19:45:02.620Z',
timestamp: '2019-01-04T14:32:03.36764-05:00',
created_at: '2020-03-14T19:45:02.620Z',
},
],
} as unknown) as Agent,
[
({
type: 'ACTION_RESULT',
subtype: 'CONFIG',
timestamp: '2019-01-04T14:32:03.36764-05:00',
action_id: 'action2',
agent_id: 'id',
} as unknown) as AgentEvent,
]
);
expect(true).toBeFalsy();
} catch (e) {
expect(isBoom(e)).toBeTruthy();
}
});

it('should fail for events that have types not in the allowed acknowledgement type list', async () => {
const mockSavedObjectsClient = savedObjectsClientMock.create();
try {
await acknowledgeAgentActions(
mockSavedObjectsClient,
({
id: 'id',
type: AGENT_TYPE_PERMANENT,
actions: [
{
type: 'CONFIG_CHANGE',
id: 'action1',
sent_at: '2020-03-14T19:45:02.620Z',
timestamp: '2019-01-04T14:32:03.36764-05:00',
created_at: '2020-03-14T19:45:02.620Z',
},
],
} as unknown) as Agent,
[
({
type: 'ACTION',
subtype: 'FAILED',
timestamp: '2019-01-04T14:32:03.36764-05:00',
action_id: 'action1',
agent_id: 'id',
} as unknown) as AgentEvent,
]
);
expect(true).toBeFalsy();
} catch (e) {
expect(isBoom(e)).toBeTruthy();
}
});
});
Loading

0 comments on commit 1c968e1

Please sign in to comment.