-
Notifications
You must be signed in to change notification settings - Fork 8.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
EMT-248: add post action request handler and resources #60581
Changes from 6 commits
67d5714
510a073
26df271
e13cee0
982a518
b47d7a4
2a3054f
d343c93
74a4729
58c3420
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
/* | ||
* 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 { NewAgentActionSchema } from '../../types/models'; | ||
import { | ||
KibanaResponseFactory, | ||
RequestHandlerContext, | ||
SavedObjectsClientContract, | ||
} from 'kibana/server'; | ||
import { savedObjectsClientMock } from '../../../../../../src/core/server/saved_objects/service/saved_objects_client.mock'; | ||
import { httpServerMock } from '../../../../../../src/core/server/http/http_server.mocks'; | ||
import { ActionsService } from '../../services/agents'; | ||
import { AgentAction } from '../../../common/types/models'; | ||
import { postNewAgentActionHandlerBuilder } from './actions_handlers'; | ||
import { PostNewAgentActionResponse } from '../../../common/types/rest_spec'; | ||
|
||
describe('test actions handlers schema', () => { | ||
it('validate that new agent actions schema is valid', async () => { | ||
expect( | ||
NewAgentActionSchema.validate({ | ||
type: 'CONFIG_CHANGE', | ||
data: 'data', | ||
sent_at: '2020-03-14T19:45:02.620Z', | ||
}) | ||
).toBeTruthy(); | ||
}); | ||
|
||
it('validate that new agent actions schema is invalid when required properties are not provided', async () => { | ||
expect(() => { | ||
NewAgentActionSchema.validate({ | ||
data: 'data', | ||
sent_at: '2020-03-14T19:45:02.620Z', | ||
}); | ||
}).toThrowError(); | ||
}); | ||
}); | ||
|
||
describe('test actions handlers', () => { | ||
let mockResponse: jest.Mocked<KibanaResponseFactory>; | ||
let mockSavedObjectsClient: jest.Mocked<SavedObjectsClientContract>; | ||
|
||
beforeEach(() => { | ||
mockSavedObjectsClient = savedObjectsClientMock.create(); | ||
mockResponse = httpServerMock.createResponseFactory(); | ||
}); | ||
|
||
it('should succeed on valid new agent action', async () => { | ||
const mockRequest = httpServerMock.createKibanaRequest({ | ||
body: { | ||
action: { | ||
type: 'CONFIG_CHANGE', | ||
data: 'data', | ||
sent_at: '2020-03-14T19:45:02.620Z', | ||
}, | ||
}, | ||
params: { | ||
agentId: 'id', | ||
}, | ||
}); | ||
|
||
const agentAction = ({ | ||
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; | ||
|
||
const actionsService: ActionsService = { | ||
getAgent: jest.fn().mockReturnValueOnce({ | ||
id: 'agent', | ||
}), | ||
getSavedObjectsClientContract: jest.fn().mockReturnValueOnce(mockSavedObjectsClient), | ||
updateAgentActions: jest.fn().mockReturnValueOnce(agentAction), | ||
} as jest.Mocked<ActionsService>; | ||
|
||
const postNewAgentActionHandler = postNewAgentActionHandlerBuilder(actionsService); | ||
await postNewAgentActionHandler( | ||
({} as unknown) as RequestHandlerContext, | ||
mockRequest, | ||
mockResponse | ||
); | ||
|
||
const expectedAgentActionResponse = (mockResponse.ok.mock.calls[0][0] | ||
?.body as unknown) as PostNewAgentActionResponse; | ||
|
||
expect(expectedAgentActionResponse.item).toEqual(agentAction); | ||
expect(expectedAgentActionResponse.success).toEqual(true); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
/* | ||
* 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 agent actions request | ||
|
||
import { RequestHandler } from 'kibana/server'; | ||
import { TypeOf } from '@kbn/config-schema'; | ||
import { PostNewAgentActionRequestSchema } from '../../types/rest_spec'; | ||
import { ActionsService } from '../../services/agents'; | ||
import { NewAgentAction } from '../../../common/types/models'; | ||
import { PostNewAgentActionResponse } from '../../../common/types/rest_spec'; | ||
|
||
export const postNewAgentActionHandlerBuilder = function( | ||
actionsService: ActionsService | ||
): RequestHandler< | ||
TypeOf<typeof PostNewAgentActionRequestSchema.params>, | ||
undefined, | ||
TypeOf<typeof PostNewAgentActionRequestSchema.body> | ||
> { | ||
return async (context, request, response) => { | ||
try { | ||
const soClient = actionsService.getSavedObjectsClientContract(request); | ||
|
||
const agent = await actionsService.getAgent(soClient, request.params.agentId); | ||
|
||
const newAgentAction = request.body.action as NewAgentAction; | ||
|
||
const savedAgentAction = await actionsService.updateAgentActions( | ||
soClient, | ||
agent, | ||
newAgentAction | ||
); | ||
|
||
const body: PostNewAgentActionResponse = { | ||
success: true, | ||
item: savedAgentAction, | ||
}; | ||
|
||
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 }, | ||
}); | ||
} | ||
}; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,6 +22,7 @@ import { | |
PostAgentAcksRequestSchema, | ||
PostAgentUnenrollRequestSchema, | ||
GetAgentStatusRequestSchema, | ||
PostNewAgentActionRequestSchema, | ||
} from '../../types'; | ||
import { | ||
getAgentsHandler, | ||
|
@@ -37,6 +38,7 @@ import { | |
} from './handlers'; | ||
import { postAgentAcksHandlerBuilder } from './acks_handlers'; | ||
import * as AgentService from '../../services/agents'; | ||
import { postNewAgentActionHandlerBuilder } from './actions_handlers'; | ||
|
||
export const registerRoutes = (router: IRouter) => { | ||
// Get one | ||
|
@@ -111,6 +113,20 @@ export const registerRoutes = (router: IRouter) => { | |
}) | ||
); | ||
|
||
// Agent actions | ||
router.post( | ||
{ | ||
path: AGENT_API_ROUTES.ACTIONS_PATTERN, | ||
validate: PostNewAgentActionRequestSchema, | ||
options: { tags: [`access:${PLUGIN_ID}-all`] }, | ||
}, | ||
postNewAgentActionHandlerBuilder({ | ||
getAgent: AgentService.getAgent, | ||
getSavedObjectsClientContract: getInternalUserSOClient, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually we want to use the SOClient that comme from the request directly There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. okay will make the change. |
||
updateAgentActions: AgentService.updateAgentActions, | ||
}) | ||
); | ||
|
||
router.post( | ||
{ | ||
path: AGENT_API_ROUTES.UNENROLL_PATTERN, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
/* | ||
* 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 { createAgentAction, updateAgentActions } from './actions'; | ||
import { Agent, AgentAction, NewAgentAction } from '../../../common/types/models'; | ||
import { savedObjectsClientMock } from '../../../../../../src/core/server/saved_objects/service/saved_objects_client.mock'; | ||
import { AGENT_TYPE_PERMANENT } from '../../../common/constants'; | ||
|
||
interface UpdatedActions { | ||
actions: AgentAction[]; | ||
} | ||
|
||
describe('test agent actions services', () => { | ||
it('should update agent current actions with new action', async () => { | ||
const mockSavedObjectsClient = savedObjectsClientMock.create(); | ||
const newAgentAction: NewAgentAction = { | ||
type: 'CONFIG_CHANGE', | ||
data: 'data', | ||
sent_at: '2020-03-14T19:45:02.620Z', | ||
}; | ||
await updateAgentActions( | ||
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, | ||
newAgentAction | ||
); | ||
|
||
const updatedAgentActions = (mockSavedObjectsClient.update.mock | ||
.calls[0][2] as unknown) as UpdatedActions; | ||
expect(updatedAgentActions.actions.length).toEqual(2); | ||
const actualAgentAction = updatedAgentActions.actions.find(action => action?.data === 'data'); | ||
expect(actualAgentAction?.type).toEqual(newAgentAction.type); | ||
expect(actualAgentAction?.data).toEqual(newAgentAction.data); | ||
expect(actualAgentAction?.sent_at).toEqual(newAgentAction.sent_at); | ||
}); | ||
|
||
it('should create agent action from new agent action model', async () => { | ||
const newAgentAction: NewAgentAction = { | ||
type: 'CONFIG_CHANGE', | ||
data: 'data', | ||
sent_at: '2020-03-14T19:45:02.620Z', | ||
}; | ||
const now = new Date(); | ||
const agentAction = createAgentAction(now, newAgentAction); | ||
expect(agentAction.type).toEqual(newAgentAction.type); | ||
expect(agentAction.data).toEqual(newAgentAction.data); | ||
expect(agentAction.sent_at).toEqual(newAgentAction.sent_at); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
/* | ||
* 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, SavedObjectsClientContract } from 'kibana/server'; | ||
import uuid from 'uuid'; | ||
import { | ||
Agent, | ||
AgentAction, | ||
AgentSOAttributes, | ||
NewAgentAction, | ||
} from '../../../common/types/models'; | ||
import { AGENT_SAVED_OBJECT_TYPE } from '../../../common/constants'; | ||
|
||
export async function updateAgentActions( | ||
soClient: SavedObjectsClientContract, | ||
agent: Agent, | ||
newAgentAction: NewAgentAction | ||
): Promise<AgentAction> { | ||
const agentAction = createAgentAction(new Date(), newAgentAction); | ||
|
||
agent.actions.push(agentAction); | ||
|
||
await soClient.update<AgentSOAttributes>(AGENT_SAVED_OBJECT_TYPE, agent.id, { | ||
actions: agent.actions, | ||
}); | ||
|
||
return agentAction; | ||
} | ||
|
||
export function createAgentAction(createdAt: Date, newAgentAction: NewAgentAction): AgentAction { | ||
const agentAction: object = { | ||
id: uuid.v4(), | ||
created_at: createdAt.toISOString(), | ||
}; | ||
|
||
Object.assign(agentAction, ...keys(newAgentAction).map(key => ({ [key]: newAgentAction[key] }))); | ||
|
||
return agentAction as AgentAction; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given the name/purpose of this function, I think it's important we don't lose the TS typing here. I think we can keep it by doing something like function createAgentAction(
createdAt: Date,
newAgentAction: NewAgentAction
): AgentAction {
const agentAction = { id: "uuid value", created_at: createdAt.toISOString() };
return Object.assign(agentAction, newAgentAction);
} |
||
} | ||
|
||
function keys<O extends object>(obj: O): Array<keyof O> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe we can drop this if we alter the way |
||
return Object.keys(obj) as Array<keyof O>; | ||
} | ||
|
||
export interface ActionsService { | ||
getAgent: (soClient: SavedObjectsClientContract, agentId: string) => Promise<Agent>; | ||
|
||
getSavedObjectsClientContract: (kibanaRequest: KibanaRequest) => SavedObjectsClientContract; | ||
|
||
updateAgentActions: ( | ||
soClient: SavedObjectsClientContract, | ||
agent: Agent, | ||
newAgentAction: NewAgentAction | ||
) => Promise<AgentAction>; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should follow the pattern used by
AgentBase
/Agent
in this file and do something likeThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay will make this change, was limiting the changes to existing structures.