-
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.
[Beats Management] APIs: Enroll beat (#19056)
* WIP checkin * Add API integration test * Converting to Jest test * Create API for enrolling a beat * Handle invalid or expired enrollment tokens * Use create instead of index to prevent same beat from being enrolled twice * Adding unit test for duplicate beat enrollment * Do not persist enrollment token with beat once token has been checked and used * Fix datatype of host_ip field * Make Kibana API guess host IP instead of requiring it in payload * Fixing error introduced in rebase conflict resolution
- Loading branch information
1 parent
4b344cc
commit b5de3c1
Showing
10 changed files
with
315 additions
and
24 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
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
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
104 changes: 104 additions & 0 deletions
104
x-pack/plugins/beats/server/routes/api/register_enroll_beat_route.js
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,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); | ||
} | ||
}); | ||
} |
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
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,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); | ||
}); | ||
}); | ||
} |
Oops, something went wrong.