Skip to content
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

1 change: 1 addition & 0 deletions x-pack/plugins/ingest_manager/common/constants/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const AGENT_API_ROUTES = {
EVENTS_PATTERN: `${FLEET_API_ROOT}/agents/{agentId}/events`,
CHECKIN_PATTERN: `${FLEET_API_ROOT}/agents/{agentId}/checkin`,
ACKS_PATTERN: `${FLEET_API_ROOT}/agents/{agentId}/acks`,
ACTIONS_PATTERN: `${FLEET_API_ROOT}/agents/{agentId}/actions`,
ENROLL_PATTERN: `${FLEET_API_ROOT}/agents/enroll`,
UNENROLL_PATTERN: `${FLEET_API_ROOT}/agents/unenroll`,
STATUS_PATTERN: `${FLEET_API_ROOT}/agent-status`,
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/ingest_manager/common/types/models/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export interface AgentAction extends SavedObjectAttributes {
sent_at?: string;
}

export type NewAgentAction = Pick<AgentAction, 'type' | 'data' | 'sent_at'>;
Copy link
Contributor

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 like

export interface NewAgentAction {
  type: 'CONFIG_CHANGE' | 'DATA_DUMP' | 'RESUME' | 'PAUSE';
  data?: string;
  sent_at?: string;
}

export interface AgentAction extends NewAgentAction {
  id: string;
  created_at: string;
}

Copy link
Contributor Author

@nnamdifrankie nnamdifrankie Mar 19, 2020

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.


export interface AgentEvent {
type: 'STATE' | 'ERROR' | 'ACTION_RESULT' | 'ACTION';
subtype: // State
Expand Down
16 changes: 15 additions & 1 deletion x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { Agent, AgentAction, AgentEvent, AgentStatus, AgentType } from '../models';
import { Agent, AgentAction, AgentEvent, AgentStatus, AgentType, NewAgentAction } from '../models';

export interface GetAgentsRequest {
query: {
Expand Down Expand Up @@ -81,6 +81,20 @@ export interface PostAgentAcksResponse {
success: boolean;
}

export interface PostNewAgentActionRequest {
body: {
action: NewAgentAction;
};
params: {
agentId: string;
};
}

export interface PostNewAgentActionResponse {
success: boolean;
item: AgentAction;
}

export interface PostAgentUnenrollRequest {
body: { kuery: string } | { ids: string[] };
}
Expand Down
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 },
});
}
};
};
16 changes: 16 additions & 0 deletions x-pack/plugins/ingest_manager/server/routes/agent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
PostAgentAcksRequestSchema,
PostAgentUnenrollRequestSchema,
GetAgentStatusRequestSchema,
PostNewAgentActionRequestSchema,
} from '../../types';
import {
getAgentsHandler,
Expand All @@ -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
Expand Down Expand Up @@ -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,
Copy link
Member

Choose a reason for hiding this comment

The 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
like this here const soClient = context.core.savedObjects.client; and not the internal user one

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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,
Expand Down
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);
});
});
58 changes: 58 additions & 0 deletions x-pack/plugins/ingest_manager/server/services/agents/actions.ts
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;
Copy link
Contributor

Choose a reason for hiding this comment

The 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> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we can drop this if we alter the way createAgentAction uses Object.assign

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>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ export * from './unenroll';
export * from './status';
export * from './crud';
export * from './update';
export * from './actions';
11 changes: 11 additions & 0 deletions x-pack/plugins/ingest_manager/server/types/models/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,14 @@ export const AckEventSchema = schema.object({
export const AgentEventSchema = schema.object({
...AgentEventBase,
});

export const NewAgentActionSchema = schema.object({
type: schema.oneOf([
schema.literal('CONFIG_CHANGE'),
schema.literal('DATA_DUMP'),
schema.literal('RESUME'),
schema.literal('PAUSE'),
]),
data: schema.maybe(schema.string()),
sent_at: schema.maybe(schema.string()),
});
Loading