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