diff --git a/x-pack/plugins/beats/common/constants/configuration_blocks.js b/x-pack/plugins/beats/common/constants/configuration_blocks.js new file mode 100644 index 0000000000000..1818b75335f3a --- /dev/null +++ b/x-pack/plugins/beats/common/constants/configuration_blocks.js @@ -0,0 +1,19 @@ +/* + * 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. + */ + +export const CONFIGURATION_BLOCKS = { + TYPES: { + OUTPUT: 'output', + PROCESSORS: 'processors', + FILEBEAT_INPUTS: 'filebeat.inputs', + FILEBEAT_MODULES: 'filebeat.modules', + METRICBEAT_MODULES: 'metricbeat.modules' + } +}; + +CONFIGURATION_BLOCKS.UNIQUENESS_ENFORCING_TYPES = [ + CONFIGURATION_BLOCKS.TYPES.OUTPUT +]; diff --git a/x-pack/plugins/beats/common/constants/index.js b/x-pack/plugins/beats/common/constants/index.js index 9fb8dffacad92..77c41be579c33 100644 --- a/x-pack/plugins/beats/common/constants/index.js +++ b/x-pack/plugins/beats/common/constants/index.js @@ -6,3 +6,4 @@ export { PLUGIN } from './plugin'; export { INDEX_NAMES } from './index_names'; +export { CONFIGURATION_BLOCKS } from './configuration_blocks'; diff --git a/x-pack/plugins/beats/server/routes/api/index.js b/x-pack/plugins/beats/server/routes/api/index.js index 4e6ee318668bf..3b0c96834aa3a 100644 --- a/x-pack/plugins/beats/server/routes/api/index.js +++ b/x-pack/plugins/beats/server/routes/api/index.js @@ -9,6 +9,7 @@ import { registerEnrollBeatRoute } from './register_enroll_beat_route'; import { registerListBeatsRoute } from './register_list_beats_route'; import { registerVerifyBeatsRoute } from './register_verify_beats_route'; import { registerUpdateBeatRoute } from './register_update_beat_route'; +import { registerCreateConfigurationBlockRoute } from './register_create_configuration_block_route'; export function registerApiRoutes(server) { registerCreateEnrollmentTokensRoute(server); @@ -16,4 +17,5 @@ export function registerApiRoutes(server) { registerListBeatsRoute(server); registerVerifyBeatsRoute(server); registerUpdateBeatRoute(server); + registerCreateConfigurationBlockRoute(server); } diff --git a/x-pack/plugins/beats/server/routes/api/register_create_configuration_block_route.js b/x-pack/plugins/beats/server/routes/api/register_create_configuration_block_route.js new file mode 100644 index 0000000000000..d259b705cf747 --- /dev/null +++ b/x-pack/plugins/beats/server/routes/api/register_create_configuration_block_route.js @@ -0,0 +1,105 @@ +/* + * 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 { get } from 'lodash'; +import { + INDEX_NAMES, + CONFIGURATION_BLOCKS +} from '../../../common/constants'; +import { callWithRequestFactory } from '../../lib/client'; +import { wrapEsError } from '../../lib/error_wrappers'; + +async function getConfigurationBlocksForTag(callWithRequest, tag) { + const params = { + index: INDEX_NAMES.BEATS, + type: '_doc', + q: `type:configuration_block AND configuration_block.tag:${tag}`, + size: 10000, + ignore: [ 404 ] + }; + + const response = await callWithRequest('search', params); + return get(response, 'hits.hits', []).map(hit => hit._source.configuration_block); +} + +function validateUniquenessEnforcingTypes(configurationBlocks, configurationBlockBeingValidated) { + const { type, tag } = configurationBlockBeingValidated; + // If the configuration block being validated is not of a uniqueness-enforcing type, then + // we don't need to perform any further validation checks. + if (!CONFIGURATION_BLOCKS.UNIQUENESS_ENFORCING_TYPES.includes(type)) { + return { isValid: true }; + } + + const isValid = !configurationBlocks.map(block => block.type).includes(type); + return { + isValid, + message: isValid + ? null + : `Configuration block for tag = ${tag} and type = ${type} already exists` + }; +} + +async function validateConfigurationBlock(callWithRequest, configurationBlockBeingValidated) { + const configurationBlocks = await getConfigurationBlocksForTag(callWithRequest, configurationBlockBeingValidated.tag); + return validateUniquenessEnforcingTypes(configurationBlocks, configurationBlockBeingValidated); +} + +function persistConfigurationBlock(callWithRequest, configurationBlock, configurationBlockId) { + const body = { + type: 'configuration_block', + configuration_block: configurationBlock + }; + + const params = { + index: INDEX_NAMES.BEATS, + type: '_doc', + id: `configuration_block:${configurationBlockId}`, + body, + refresh: 'wait_for' + }; + + return callWithRequest('create', params); +} + +// TODO: add license check pre-hook +// TODO: write to Kibana audit log file +export function registerCreateConfigurationBlockRoute(server) { + server.route({ + method: 'POST', + path: '/api/beats/configuration_blocks', + config: { + validate: { + payload: Joi.object({ + type: Joi.string().required().valid(Object.values(CONFIGURATION_BLOCKS.TYPES)), + tag: Joi.string().required(), + block_yml: Joi.string().required() + }).required() + } + }, + handler: async (request, reply) => { + const callWithRequest = callWithRequestFactory(server, request); + + let configurationBlockId; + try { + const configurationBlock = request.payload; + const { isValid, message } = await validateConfigurationBlock(callWithRequest, configurationBlock); + if (!isValid) { + return reply({ message }).code(400); + } + + configurationBlockId = uuid.v4(); + await persistConfigurationBlock(callWithRequest, request.payload, configurationBlockId); + } catch (err) { + return reply(wrapEsError(err)); + } + + const response = { id: configurationBlockId }; + reply(response).code(201); + } + }); +} diff --git a/x-pack/test/api_integration/apis/beats/create_configuration_block.js b/x-pack/test/api_integration/apis/beats/create_configuration_block.js new file mode 100644 index 0000000000000..680ff8b2a6d21 --- /dev/null +++ b/x-pack/test/api_integration/apis/beats/create_configuration_block.js @@ -0,0 +1,149 @@ +/* + * 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 { + ES_INDEX_NAME, + ES_TYPE_NAME +} from './constants'; + +export default function ({ getService }) { + const supertest = getService('supertest'); + const es = getService('es'); + const chance = getService('chance'); + + describe('create_configuration_block', () => { + it('should create the given configuration block', async () => { + const configurationBlock = { + type: 'output', + tag: 'production', + block_yml: 'elasticsearch:\n hosts: [\"localhost:9200\"]\n username: "..."' + }; + const { body: apiResponse } = await supertest + .post( + '/api/beats/configuration_blocks' + ) + .set('kbn-xsrf', 'xxx') + .send(configurationBlock) + .expect(201); + + const idFromApi = apiResponse.id; + + const esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `configuration_block:${idFromApi}` + }); + + const docInEs = esResponse._source; + + expect(docInEs.type).to.eql('configuration_block'); + expect(docInEs.configuration_block.type).to.eql(configurationBlock.type); + expect(docInEs.configuration_block.tag).to.eql(configurationBlock.tag); + expect(docInEs.configuration_block.block_yml).to.eql(configurationBlock.block_yml); + }); + + it('should not allow two "output" type configuration blocks with the same tag', async () => { + const firstConfigurationBlock = { + type: 'output', + tag: 'production', + block_yml: 'elasticsearch:\n hosts: ["localhost:9200"]\n username: "..."' + }; + await supertest + .post( + '/api/beats/configuration_blocks' + ) + .set('kbn-xsrf', 'xxx') + .send(firstConfigurationBlock) + .expect(201); + + const secondConfigurationBlock = { + type: 'output', + tag: 'production', + block_yml: 'logstash:\n hosts: ["localhost:9000"]\n' + }; + await supertest + .post( + '/api/beats/configuration_blocks' + ) + .set('kbn-xsrf', 'xxx') + .send(secondConfigurationBlock) + .expect(400); + }); + + it('should allow two "output" type configuration blocks with different tags', async () => { + const firstConfigurationBlock = { + type: 'output', + tag: 'production', + block_yml: 'elasticsearch:\n hosts: ["localhost:9200"]\n username: "..."' + }; + await supertest + .post( + '/api/beats/configuration_blocks' + ) + .set('kbn-xsrf', 'xxx') + .send(firstConfigurationBlock) + .expect(201); + + const secondConfigurationBlock = { + type: 'output', + tag: 'development', + block_yml: 'logstash:\n hosts: ["localhost:9000"]\n' + }; + await supertest + .post( + '/api/beats/configuration_blocks' + ) + .set('kbn-xsrf', 'xxx') + .send(secondConfigurationBlock) + .expect(201); + }); + + it('should allow two configuration blocks of different types with the same tag', async () => { + const firstConfigurationBlock = { + type: 'output', + tag: 'production', + block_yml: 'elasticsearch:\n hosts: ["localhost:9200"]\n username: "..."' + }; + await supertest + .post( + '/api/beats/configuration_blocks' + ) + .set('kbn-xsrf', 'xxx') + .send(firstConfigurationBlock) + .expect(201); + + const secondConfigurationBlock = { + type: 'filebeat.inputs', + tag: 'production', + block_yml: 'file:\n path: "/var/log/some.log"]\n' + }; + await supertest + .post( + '/api/beats/configuration_blocks' + ) + .set('kbn-xsrf', 'xxx') + .send(secondConfigurationBlock) + .expect(201); + }); + + + it('should reject a configuration block with an invalid type', async () => { + const firstConfigurationBlock = { + type: chance.word(), + tag: 'production', + block_yml: 'elasticsearch:\n hosts: ["localhost:9200"]\n username: "..."' + }; + await supertest + .post( + '/api/beats/configuration_blocks' + ) + .set('kbn-xsrf', 'xxx') + .send(firstConfigurationBlock) + .expect(400); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/beats/index.js b/x-pack/test/api_integration/apis/beats/index.js index b41f17ed749b3..da94353aafeee 100644 --- a/x-pack/test/api_integration/apis/beats/index.js +++ b/x-pack/test/api_integration/apis/beats/index.js @@ -22,5 +22,6 @@ export default function ({ getService, loadTestFile }) { loadTestFile(require.resolve('./list_beats')); loadTestFile(require.resolve('./verify_beats')); loadTestFile(require.resolve('./update_beat')); + loadTestFile(require.resolve('./create_configuration_block')); }); }