Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions x-pack/plugins/beats/common/constants/configuration_blocks.js
Original file line number Diff line number Diff line change
@@ -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
];
1 change: 1 addition & 0 deletions x-pack/plugins/beats/common/constants/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@

export { PLUGIN } from './plugin';
export { INDEX_NAMES } from './index_names';
export { CONFIGURATION_BLOCKS } from './configuration_blocks';
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 @@ -9,11 +9,13 @@ 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);
registerEnrollBeatRoute(server);
registerListBeatsRoute(server);
registerVerifyBeatsRoute(server);
registerUpdateBeatRoute(server);
registerCreateConfigurationBlockRoute(server);
}
Original file line number Diff line number Diff line change
@@ -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);
}
});
}
149 changes: 149 additions & 0 deletions x-pack/test/api_integration/apis/beats/create_configuration_block.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
}
1 change: 1 addition & 0 deletions x-pack/test/api_integration/apis/beats/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
});
}