-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
EMT-248: implement ack resource to accept event payload to acknowledg…
…e agent actions (#60218) [Ingest]EMT-248: implement ack resource to accept event payload to acknowledge agent actions
- Loading branch information
1 parent
c898e79
commit 69ec60d
Showing
12 changed files
with
539 additions
and
56 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
94 changes: 94 additions & 0 deletions
94
x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
69
x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }, | ||
}); | ||
} | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
118 changes: 118 additions & 0 deletions
118
x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
}); | ||
}); |
Oops, something went wrong.