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

[Beats Management] APIs: Enroll beat #19056

Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@
import { once } from 'lodash';

const callWithRequest = once((server) => {
const config = server.config().get('elasticsearch');
const cluster = server.plugins.elasticsearch.createCluster('beats', config);
return cluster.callWithRequest;
const { callWithRequest } = server.plugins.elasticsearch.getCluster('admin');
return callWithRequest;
});

export const callWithRequestFactory = (server, request) => {
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/beats/server/lib/client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
* you may not use this file except in compliance with the Elastic License.
*/

export { callWithRequestFactory } from './call_with_request_factory';
export { callWithInternalUserFactory } from './call_with_internal_user_factory';
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,6 @@
"id": {
"type": "keyword"
},
"enrollment_token": {
"type": "keyword"
},
"access_token": {
"type": "keyword"
},
Expand All @@ -58,7 +55,7 @@
"type": "keyword"
},
"host_ip": {
"type": "keyword"
"type": "ip"
},
"host_name": {
"type": "keyword"
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/beats/server/routes/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
*/

import { registerCreateEnrollmentTokensRoute } from './register_create_enrollment_tokens_route';
import { registerEnrollBeatRoute } from './register_enroll_beat_route';

export function registerApiRoutes(server) {
registerCreateEnrollmentTokensRoute(server);
registerEnrollBeatRoute(server);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
flatten
} from 'lodash';
import { INDEX_NAMES } from '../../../common/constants';
import { callWithRequestFactory } from '../../lib/call_with_request_factory';
import { callWithRequestFactory } from '../../lib/client';
import { wrapEsError } from '../../lib/error_wrappers';

function persistTokens(callWithRequest, tokens, enrollmentTokensTtlInSeconds) {
Expand Down
104 changes: 104 additions & 0 deletions x-pack/plugins/beats/server/routes/api/register_enroll_beat_route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* 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 Joi from 'joi';
import uuid from 'uuid';
import moment from 'moment';
import {
get,
omit
} from 'lodash';
import { INDEX_NAMES } from '../../../common/constants';
import { callWithInternalUserFactory } from '../../lib/client';
import { wrapEsError } from '../../lib/error_wrappers';

async function getEnrollmentToken(callWithInternalUser, enrollmentToken) {
const params = {
index: INDEX_NAMES.BEATS,
type: '_doc',
id: `enrollment_token:${enrollmentToken}`,
ignore: [ 404 ]
};

const response = await callWithInternalUser('get', params);
return get(response, '_source.enrollment_token', {});
}

function deleteUsedEnrollmentToken(callWithInternalUser, enrollmentToken) {
const params = {
index: INDEX_NAMES.BEATS,
type: '_doc',
id: `enrollment_token:${enrollmentToken}`
};

return callWithInternalUser('delete', params);
}

function persistBeat(callWithInternalUser, beat, beatId, accessToken, remoteAddress) {
const body = {
type: 'beat',
beat: {
...omit(beat, 'enrollment_token'),
id: beatId,
access_token: accessToken,
host_ip: remoteAddress
}
};

const params = {
index: INDEX_NAMES.BEATS,
type: '_doc',
id: `beat:${beatId}`,
body,
refresh: 'wait_for'
};
return callWithInternalUser('create', params);
}

// TODO: add license check pre-hook
// TODO: write to Kibana audit log file
export function registerEnrollBeatRoute(server) {
server.route({
method: 'POST',
path: '/api/beats/agent/{beatId}',
config: {
validate: {
payload: Joi.object({
enrollment_token: Joi.string().required(),
type: Joi.string().required(),
host_name: Joi.string().required()
}).required()
},
auth: false
},
handler: async (request, reply) => {
const callWithInternalUser = callWithInternalUserFactory(server);
let accessToken;

try {
const enrollmentToken = request.payload.enrollment_token;
const { token, expires_on: expiresOn } = await getEnrollmentToken(callWithInternalUser, enrollmentToken);
if (!token || token !== enrollmentToken) {
return reply({ message: 'Invalid enrollment token' }).code(400);
}
if (moment(expiresOn).isBefore(moment())) {
return reply({ message: 'Expired enrollment token' }).code(400);
}

accessToken = uuid.v4().replace(/-/g, "");
const remoteAddress = request.info.remoteAddress;
await persistBeat(callWithInternalUser, request.payload, request.params.beatId, accessToken, remoteAddress);

await deleteUsedEnrollmentToken(callWithInternalUser, enrollmentToken);
} catch (err) {
return reply(wrapEsError(err));
}

const response = { access_token: accessToken };
reply(response).code(201);
}
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/

export { callWithRequestFactory } from './call_with_request_factory';
export const ES_INDEX_NAME = '.management-beats';
export const ES_TYPE_NAME = '_doc';

19 changes: 5 additions & 14 deletions x-pack/test/api_integration/apis/beats/create_enrollment_tokens.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,17 @@

import expect from 'expect.js';
import moment from 'moment';
import {
ES_INDEX_NAME,
ES_TYPE_NAME
} from './constants';

export default function ({ getService }) {
const supertest = getService('supertest');
const chance = getService('chance');
const es = getService('es');

const ES_INDEX_NAME = '.management-beats';
const ES_TYPE_NAME = '_doc';

describe('create_enrollment_tokens', () => {
const cleanup = () => {
return es.indices.delete({
index: ES_INDEX_NAME,
ignore: [ 404 ]
});
};

beforeEach(cleanup);
afterEach(cleanup);

describe('create_enrollment_token', () => {
it('should create one token by default', async () => {
const { body: apiResponse } = await supertest
.post(
Expand Down
183 changes: 183 additions & 0 deletions x-pack/test/api_integration/apis/beats/enroll_beat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/*
* 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 expect from 'expect.js';
import moment from 'moment';
import {
ES_INDEX_NAME,
ES_TYPE_NAME
} from './constants';

export default function ({ getService }) {
const supertest = getService('supertest');
const chance = getService('chance');
const es = getService('es');

describe('enroll_beat', () => {
let validEnrollmentToken;
let beatId;
let beat;

beforeEach(async () => {
validEnrollmentToken = chance.word();
beatId = chance.word();
beat = {
enrollment_token: validEnrollmentToken,
type: 'filebeat',
host_name: 'foo.bar.com',
};

await es.index({
index: ES_INDEX_NAME,
type: ES_TYPE_NAME,
id: `enrollment_token:${validEnrollmentToken}`,
body: {
type: 'enrollment_token',
enrollment_token: {
token: validEnrollmentToken,
expires_on: moment().add(4, 'hours').toJSON()
}
}
});
});

it('should enroll beat in an unverified state', async () => {
await supertest
.post(
`/api/beats/agent/${beatId}`
)
.set('kbn-xsrf', 'xxx')
.send(beat)
.expect(201);

const esResponse = await es.get({
index: ES_INDEX_NAME,
type: ES_TYPE_NAME,
id: `beat:${beatId}`
});

expect(esResponse._source.beat).to.not.have.property('verified_on');
expect(esResponse._source.beat).to.have.property('host_ip');
});

it('should contain an access token in the response', async () => {
const { body: apiResponse } = await supertest
.post(
`/api/beats/agent/${beatId}`
)
.set('kbn-xsrf', 'xxx')
.send(beat)
.expect(201);

const accessTokenFromApi = apiResponse.access_token;

const esResponse = await es.get({
index: ES_INDEX_NAME,
type: ES_TYPE_NAME,
id: `beat:${beatId}`
});

const accessTokenInEs = esResponse._source.beat.access_token;

expect(accessTokenFromApi.length).to.be.greaterThan(0);
expect(accessTokenFromApi).to.eql(accessTokenInEs);
});

it('should reject an invalid enrollment token', async () => {
const invalidEnrollmentToken = chance.word();
beat.enrollment_token = invalidEnrollmentToken;

const { body: apiResponse } = await supertest
.post(
`/api/beats/agent/${beatId}`
)
.set('kbn-xsrf', 'xxx')
.send(beat)
.expect(400);

expect(apiResponse).to.eql({ message: 'Invalid enrollment token' });
});

it('should reject an expired enrollment token', async () => {
const expiredEnrollmentToken = chance.word();

await es.index({
index: ES_INDEX_NAME,
type: ES_TYPE_NAME,
id: `enrollment_token:${expiredEnrollmentToken}`,
body: {
type: 'enrollment_token',
enrollment_token: {
token: expiredEnrollmentToken,
expires_on: moment().subtract(1, 'minute').toJSON()
}
}
});

beat.enrollment_token = expiredEnrollmentToken;

const { body: apiResponse } = await supertest
.post(
`/api/beats/agent/${beatId}`
)
.set('kbn-xsrf', 'xxx')
.send(beat)
.expect(400);

expect(apiResponse).to.eql({ message: 'Expired enrollment token' });
});

it('should delete the given enrollment token so it may not be reused', async () => {
await supertest
.post(
`/api/beats/agent/${beatId}`
)
.set('kbn-xsrf', 'xxx')
.send(beat)
.expect(201);

const esResponse = await es.get({
index: ES_INDEX_NAME,
type: ES_TYPE_NAME,
id: `enrollment_token:${validEnrollmentToken}`,
ignore: [ 404 ]
});

expect(esResponse.found).to.be(false);
});

it('should fail if the beat with the same ID is enrolled twice', async () => {
await supertest
.post(
`/api/beats/agent/${beatId}`
)
.set('kbn-xsrf', 'xxx')
.send(beat)
.expect(201);

await es.index({
index: ES_INDEX_NAME,
type: ES_TYPE_NAME,
id: `enrollment_token:${validEnrollmentToken}`,
body: {
type: 'enrollment_token',
enrollment_token: {
token: validEnrollmentToken,
expires_on: moment().add(4, 'hours').toJSON()
}
}
});

await supertest
.post(
`/api/beats/agent/${beatId}`
)
.set('kbn-xsrf', 'xxx')
.send(beat)
.expect(409);
});
});
}
Loading