From ae0f49658a52466d7c44dd2aec1d6a820fddde7a Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Tue, 7 Aug 2018 14:11:08 +0100 Subject: [PATCH 01/43] [ML] Fixing issue with historical job audit messages (#21718) --- .../ml/server/models/job_audit_messages/job_audit_messages.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.js b/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.js index 0b0524acd58bf..3a6a61474f8dd 100644 --- a/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.js +++ b/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.js @@ -20,8 +20,8 @@ export function jobAuditMessagesProvider(callWithRequest) { let gte = null; if (jobId !== undefined && from === undefined) { const jobs = await callWithRequest('ml.jobs', { jobId }); - if (jobs.length) { - gte = moment(jobs[0].create_time).valueOf(); + if (jobs.count > 0 && jobs.jobs !== undefined) { + gte = moment(jobs.jobs[0].create_time).valueOf(); } } else if (from !== undefined) { gte = `now-${from}`; From dc0b7074d4657d4f545d39847c5090ca1c03ec36 Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Tue, 7 Aug 2018 15:11:55 +0200 Subject: [PATCH 02/43] Add proper aria-label for close inspector (#21719) --- src/ui/public/flyout/flyout_session.tsx | 1 + src/ui/public/inspector/inspector.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/src/ui/public/flyout/flyout_session.tsx b/src/ui/public/flyout/flyout_session.tsx index 185c7378fa86a..58bb5d625ed31 100644 --- a/src/ui/public/flyout/flyout_session.tsx +++ b/src/ui/public/flyout/flyout_session.tsx @@ -85,6 +85,7 @@ class FlyoutSession extends EventEmitter { export function openFlyout( flyoutChildren: React.ReactNode, flyoutProps: { + closeButtonAriaLabel?: string; onClose?: () => void; 'data-test-subj'?: string; } = {} diff --git a/src/ui/public/inspector/inspector.tsx b/src/ui/public/inspector/inspector.tsx index 7d19674945a6a..1fa3d7f54c909 100644 --- a/src/ui/public/inspector/inspector.tsx +++ b/src/ui/public/inspector/inspector.tsx @@ -69,6 +69,7 @@ function open(adapters: Adapters, options: InspectorOptions = {}): InspectorSess return openFlyout(, { 'data-test-subj': 'inspectorPanel', + closeButtonAriaLabel: 'Close Inspector', }); } From 4cf33082579f9bf01eff1b6b4607b1b7ab8e097d Mon Sep 17 00:00:00 2001 From: Shaunak Kashyap Date: Fri, 11 May 2018 09:43:21 -0700 Subject: [PATCH 03/43] [Beats Management] Initial scaffolding for plugin (#18977) * Initial scaffolding for Beats plugin * Removing bits not (yet) necessary in initial scaffolding --- x-pack/index.js | 2 ++ x-pack/plugins/beats/common/constants/index.js | 7 +++++++ x-pack/plugins/beats/common/constants/plugin.js | 9 +++++++++ x-pack/plugins/beats/index.js | 16 ++++++++++++++++ 4 files changed, 34 insertions(+) create mode 100644 x-pack/plugins/beats/common/constants/index.js create mode 100644 x-pack/plugins/beats/common/constants/plugin.js create mode 100644 x-pack/plugins/beats/index.js diff --git a/x-pack/index.js b/x-pack/index.js index a98af06dde131..6f5c12814997a 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -16,6 +16,7 @@ import { watcher } from './plugins/watcher'; import { grokdebugger } from './plugins/grokdebugger'; import { dashboardMode } from './plugins/dashboard_mode'; import { logstash } from './plugins/logstash'; +import { beats } from './plugins/beats'; import { apm } from './plugins/apm'; import { licenseManagement } from './plugins/license_management'; import { cloud } from './plugins/cloud'; @@ -38,6 +39,7 @@ module.exports = function (kibana) { grokdebugger(kibana), dashboardMode(kibana), logstash(kibana), + beats(kibana), apm(kibana), licenseManagement(kibana), cloud(kibana), diff --git a/x-pack/plugins/beats/common/constants/index.js b/x-pack/plugins/beats/common/constants/index.js new file mode 100644 index 0000000000000..035454fd7b435 --- /dev/null +++ b/x-pack/plugins/beats/common/constants/index.js @@ -0,0 +1,7 @@ +/* + * 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 { PLUGIN } from './plugin'; diff --git a/x-pack/plugins/beats/common/constants/plugin.js b/x-pack/plugins/beats/common/constants/plugin.js new file mode 100644 index 0000000000000..289bc488c58a6 --- /dev/null +++ b/x-pack/plugins/beats/common/constants/plugin.js @@ -0,0 +1,9 @@ +/* + * 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 PLUGIN = { + ID: 'beats' +}; diff --git a/x-pack/plugins/beats/index.js b/x-pack/plugins/beats/index.js new file mode 100644 index 0000000000000..8319485db1749 --- /dev/null +++ b/x-pack/plugins/beats/index.js @@ -0,0 +1,16 @@ +/* + * 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 { PLUGIN } from './common/constants'; + +export function beats(kibana) { + return new kibana.Plugin({ + id: PLUGIN.ID, + require: ['kibana', 'elasticsearch', 'xpack_main'], + init: function () { + } + }); +} From 88dcee611f149bd5462d37a5adf71e88352b84b5 Mon Sep 17 00:00:00 2001 From: Shaunak Kashyap Date: Tue, 15 May 2018 11:29:46 -0700 Subject: [PATCH 04/43] [Beats Management] Install Beats index template on plugin init (#19072) * Install Beats index template on plugin init * Adding missing files --- x-pack/plugins/beats/index.js | 4 +- .../client/call_with_internal_user_factory.js | 16 ++++ .../plugins/beats/server/lib/client/index.js | 7 ++ .../lib/index_template/beats_template.json | 84 +++++++++++++++++++ .../beats/server/lib/index_template/index.js | 7 ++ .../index_template/install_index_template.js | 18 ++++ 6 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/beats/server/lib/client/call_with_internal_user_factory.js create mode 100644 x-pack/plugins/beats/server/lib/client/index.js create mode 100644 x-pack/plugins/beats/server/lib/index_template/beats_template.json create mode 100644 x-pack/plugins/beats/server/lib/index_template/index.js create mode 100644 x-pack/plugins/beats/server/lib/index_template/install_index_template.js diff --git a/x-pack/plugins/beats/index.js b/x-pack/plugins/beats/index.js index 8319485db1749..8839a30fc5a2e 100644 --- a/x-pack/plugins/beats/index.js +++ b/x-pack/plugins/beats/index.js @@ -4,13 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ +import { installIndexTemplate } from './server/lib/index_template'; import { PLUGIN } from './common/constants'; export function beats(kibana) { return new kibana.Plugin({ id: PLUGIN.ID, require: ['kibana', 'elasticsearch', 'xpack_main'], - init: function () { + init: async function (server) { + await installIndexTemplate(server); } }); } diff --git a/x-pack/plugins/beats/server/lib/client/call_with_internal_user_factory.js b/x-pack/plugins/beats/server/lib/client/call_with_internal_user_factory.js new file mode 100644 index 0000000000000..8b5dbed773430 --- /dev/null +++ b/x-pack/plugins/beats/server/lib/client/call_with_internal_user_factory.js @@ -0,0 +1,16 @@ +/* + * 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 { once } from 'lodash'; + +const callWithInternalUser = once((server) => { + const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin'); + return callWithInternalUser; +}); + +export const callWithInternalUserFactory = (server) => { + return callWithInternalUser(server); +}; diff --git a/x-pack/plugins/beats/server/lib/client/index.js b/x-pack/plugins/beats/server/lib/client/index.js new file mode 100644 index 0000000000000..a56a50e2864a5 --- /dev/null +++ b/x-pack/plugins/beats/server/lib/client/index.js @@ -0,0 +1,7 @@ +/* + * 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 { callWithInternalUserFactory } from './call_with_internal_user_factory'; diff --git a/x-pack/plugins/beats/server/lib/index_template/beats_template.json b/x-pack/plugins/beats/server/lib/index_template/beats_template.json new file mode 100644 index 0000000000000..6ef57b9c549ed --- /dev/null +++ b/x-pack/plugins/beats/server/lib/index_template/beats_template.json @@ -0,0 +1,84 @@ +{ + "index_patterns": [ + ".management-beats" + ], + "version": 65000, + "settings": { + "index": { + "number_of_shards": 1, + "auto_expand_replicas": "0-1", + "codec": "best_compression" + } + }, + "mappings": { + "_doc": { + "dynamic": "strict", + "properties": { + "type": { + "type": "keyword" + }, + "enrollment_token": { + "properties": { + "token": { + "type": "keyword" + }, + "expires_on": { + "type": "date" + } + } + }, + "configuration_block": { + "properties": { + "tag": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "block_yml": { + "type": "text" + } + } + }, + "beat": { + "properties": { + "id": { + "type": "keyword" + }, + "enrollment_token": { + "type": "keyword" + }, + "access_token": { + "type": "keyword" + }, + "verified_on": { + "type": "date" + }, + "type": { + "type": "keyword" + }, + "host_ip": { + "type": "keyword" + }, + "host_name": { + "type": "keyword" + }, + "ephemeral_id": { + "type": "keyword" + }, + "local_configuration_yml": { + "type": "text" + }, + "central_configuration_yml": { + "type": "text" + }, + "metadata": { + "dynamic": "true", + "type": "object" + } + } + } + } + } + } +} diff --git a/x-pack/plugins/beats/server/lib/index_template/index.js b/x-pack/plugins/beats/server/lib/index_template/index.js new file mode 100644 index 0000000000000..04128e46ff0ea --- /dev/null +++ b/x-pack/plugins/beats/server/lib/index_template/index.js @@ -0,0 +1,7 @@ +/* + * 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 { installIndexTemplate } from './install_index_template'; diff --git a/x-pack/plugins/beats/server/lib/index_template/install_index_template.js b/x-pack/plugins/beats/server/lib/index_template/install_index_template.js new file mode 100644 index 0000000000000..01b080903ccac --- /dev/null +++ b/x-pack/plugins/beats/server/lib/index_template/install_index_template.js @@ -0,0 +1,18 @@ +/* + * 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 beatsIndexTemplate from './beats_template'; +import { callWithInternalUserFactory } from '../client'; + +const TEMPLATE_NAME = 'beats-template'; + +export function installIndexTemplate(server) { + const callWithInternalUser = callWithInternalUserFactory(server); + return callWithInternalUser('indices.putTemplate', { + name: TEMPLATE_NAME, + body: beatsIndexTemplate + }); +} From cc7759f952d2304db0d595af8072ae69c5d7e039 Mon Sep 17 00:00:00 2001 From: Shaunak Kashyap Date: Tue, 15 May 2018 12:46:32 -0700 Subject: [PATCH 05/43] [Beats Management] APIs: Create enrollment tokens (#19018) * WIP checkin * Register API routes * Fixing typo in index name * Adding TODOs * Removing commented out license checking code that isn't yet implemented * Remove unnecessary async/await * Don't return until indices have been refreshed * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Adding TODO * Fixing variable name * Using a single index * Adding expiration date field * Adding test for expiration date field * Ignore non-existent index * Fixing logic in test * Creating constant for default enrollment tokens TTL value * Updating test --- .../plugins/beats/common/constants/index.js | 1 + .../beats/common/constants/index_names.js | 9 ++ x-pack/plugins/beats/index.js | 9 ++ .../call_with_request_factory.js | 19 +++ .../lib/call_with_request_factory/index.js | 7 ++ .../beats/server/lib/error_wrappers/index.js | 7 ++ .../lib/error_wrappers/wrap_es_error.js | 22 ++++ .../lib/error_wrappers/wrap_es_error.test.js | 40 +++++++ .../plugins/beats/server/routes/api/index.js | 11 ++ ...register_create_enrollment_tokens_route.js | 70 +++++++++++ .../apis/beats/create_enrollment_token.js | 109 ++++++++++++++++++ .../test/api_integration/apis/beats/index.js | 11 ++ x-pack/test/api_integration/apis/index.js | 1 + x-pack/test/api_integration/config.js | 1 + 14 files changed, 317 insertions(+) create mode 100644 x-pack/plugins/beats/common/constants/index_names.js create mode 100644 x-pack/plugins/beats/server/lib/call_with_request_factory/call_with_request_factory.js create mode 100644 x-pack/plugins/beats/server/lib/call_with_request_factory/index.js create mode 100644 x-pack/plugins/beats/server/lib/error_wrappers/index.js create mode 100644 x-pack/plugins/beats/server/lib/error_wrappers/wrap_es_error.js create mode 100644 x-pack/plugins/beats/server/lib/error_wrappers/wrap_es_error.test.js create mode 100644 x-pack/plugins/beats/server/routes/api/index.js create mode 100644 x-pack/plugins/beats/server/routes/api/register_create_enrollment_tokens_route.js create mode 100644 x-pack/test/api_integration/apis/beats/create_enrollment_token.js create mode 100644 x-pack/test/api_integration/apis/beats/index.js diff --git a/x-pack/plugins/beats/common/constants/index.js b/x-pack/plugins/beats/common/constants/index.js index 035454fd7b435..9fb8dffacad92 100644 --- a/x-pack/plugins/beats/common/constants/index.js +++ b/x-pack/plugins/beats/common/constants/index.js @@ -5,3 +5,4 @@ */ export { PLUGIN } from './plugin'; +export { INDEX_NAMES } from './index_names'; diff --git a/x-pack/plugins/beats/common/constants/index_names.js b/x-pack/plugins/beats/common/constants/index_names.js new file mode 100644 index 0000000000000..e63e8b08a6ef4 --- /dev/null +++ b/x-pack/plugins/beats/common/constants/index_names.js @@ -0,0 +1,9 @@ +/* + * 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 INDEX_NAMES = { + BEATS: '.management-beats' +}; diff --git a/x-pack/plugins/beats/index.js b/x-pack/plugins/beats/index.js index 8839a30fc5a2e..c105813e36ff6 100644 --- a/x-pack/plugins/beats/index.js +++ b/x-pack/plugins/beats/index.js @@ -5,14 +5,23 @@ */ import { installIndexTemplate } from './server/lib/index_template'; +import { registerApiRoutes } from './server/routes/api'; import { PLUGIN } from './common/constants'; +const DEFAULT_ENROLLMENT_TOKENS_TTL_S = 10 * 60; // 10 minutes + export function beats(kibana) { return new kibana.Plugin({ id: PLUGIN.ID, require: ['kibana', 'elasticsearch', 'xpack_main'], + configPrefix: 'xpack.beats', + config: Joi => Joi.object({ + enabled: Joi.boolean().default(true), + enrollmentTokensTtlInSeconds: Joi.number().integer().min(1).default(DEFAULT_ENROLLMENT_TOKENS_TTL_S) + }).default(), init: async function (server) { await installIndexTemplate(server); + registerApiRoutes(server); } }); } diff --git a/x-pack/plugins/beats/server/lib/call_with_request_factory/call_with_request_factory.js b/x-pack/plugins/beats/server/lib/call_with_request_factory/call_with_request_factory.js new file mode 100644 index 0000000000000..0c4f909d12f61 --- /dev/null +++ b/x-pack/plugins/beats/server/lib/call_with_request_factory/call_with_request_factory.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. + */ + +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; +}); + +export const callWithRequestFactory = (server, request) => { + return (...args) => { + return callWithRequest(server)(request, ...args); + }; +}; diff --git a/x-pack/plugins/beats/server/lib/call_with_request_factory/index.js b/x-pack/plugins/beats/server/lib/call_with_request_factory/index.js new file mode 100644 index 0000000000000..787814d87dff9 --- /dev/null +++ b/x-pack/plugins/beats/server/lib/call_with_request_factory/index.js @@ -0,0 +1,7 @@ +/* + * 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 { callWithRequestFactory } from './call_with_request_factory'; diff --git a/x-pack/plugins/beats/server/lib/error_wrappers/index.js b/x-pack/plugins/beats/server/lib/error_wrappers/index.js new file mode 100644 index 0000000000000..3756b0c74fb10 --- /dev/null +++ b/x-pack/plugins/beats/server/lib/error_wrappers/index.js @@ -0,0 +1,7 @@ +/* + * 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 { wrapEsError } from './wrap_es_error'; diff --git a/x-pack/plugins/beats/server/lib/error_wrappers/wrap_es_error.js b/x-pack/plugins/beats/server/lib/error_wrappers/wrap_es_error.js new file mode 100644 index 0000000000000..d2abcab5c37dd --- /dev/null +++ b/x-pack/plugins/beats/server/lib/error_wrappers/wrap_es_error.js @@ -0,0 +1,22 @@ +/* + * 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 Boom from 'boom'; + +/** + * Wraps ES errors into a Boom error response and returns it + * This also handles the permissions issue gracefully + * + * @param err Object ES error + * @return Object Boom error response + */ +export function wrapEsError(err) { + const statusCode = err.statusCode; + if (statusCode === 403) { + return Boom.forbidden('Insufficient user permissions for managing Beats configuration'); + } + return Boom.wrap(err, err.statusCode); +} diff --git a/x-pack/plugins/beats/server/lib/error_wrappers/wrap_es_error.test.js b/x-pack/plugins/beats/server/lib/error_wrappers/wrap_es_error.test.js new file mode 100644 index 0000000000000..ec7338262844a --- /dev/null +++ b/x-pack/plugins/beats/server/lib/error_wrappers/wrap_es_error.test.js @@ -0,0 +1,40 @@ +/* + * 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 { wrapEsError } from './wrap_es_error'; + +describe('wrap_es_error', () => { + describe('#wrapEsError', () => { + + let originalError; + beforeEach(() => { + originalError = new Error('I am an error'); + originalError.statusCode = 404; + }); + + it('should return a Boom object', () => { + const wrappedError = wrapEsError(originalError); + + expect(wrappedError.isBoom).to.be(true); + }); + + it('should return the correct Boom object', () => { + const wrappedError = wrapEsError(originalError); + + expect(wrappedError.output.statusCode).to.be(originalError.statusCode); + expect(wrappedError.output.payload.message).to.be(originalError.message); + }); + + it('should return invalid permissions message for 403 errors', () => { + const securityError = new Error('I am an error'); + securityError.statusCode = 403; + const wrappedError = wrapEsError(securityError); + + expect(wrappedError.isBoom).to.be(true); + expect(wrappedError.message).to.be('Insufficient user permissions for managing Logstash pipelines'); + }); + }); +}); diff --git a/x-pack/plugins/beats/server/routes/api/index.js b/x-pack/plugins/beats/server/routes/api/index.js new file mode 100644 index 0000000000000..8bf546045fe40 --- /dev/null +++ b/x-pack/plugins/beats/server/routes/api/index.js @@ -0,0 +1,11 @@ +/* + * 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 { registerCreateEnrollmentTokensRoute } from './register_create_enrollment_tokens_route'; + +export function registerApiRoutes(server) { + registerCreateEnrollmentTokensRoute(server); +} diff --git a/x-pack/plugins/beats/server/routes/api/register_create_enrollment_tokens_route.js b/x-pack/plugins/beats/server/routes/api/register_create_enrollment_tokens_route.js new file mode 100644 index 0000000000000..582ae59062d8b --- /dev/null +++ b/x-pack/plugins/beats/server/routes/api/register_create_enrollment_tokens_route.js @@ -0,0 +1,70 @@ +/* + * 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, + flatten +} from 'lodash'; +import { INDEX_NAMES } from '../../../common/constants'; +import { callWithRequestFactory } from '../../lib/call_with_request_factory'; +import { wrapEsError } from '../../lib/error_wrappers'; + +function persistTokens(callWithRequest, tokens, enrollmentTokensTtlInSeconds) { + const enrollmentTokenExpiration = moment().add(enrollmentTokensTtlInSeconds, 'seconds').toJSON(); + const body = flatten(tokens.map(token => [ + { index: { _id: `enrollment_token:${token}` } }, + { type: 'enrollment_token', enrollment_token: { token, expires_on: enrollmentTokenExpiration } } + ])); + + const params = { + index: INDEX_NAMES.BEATS, + type: '_doc', + body, + refresh: 'wait_for' + }; + + return callWithRequest('bulk', params); +} + +// TODO: add license check pre-hook +// TODO: write to Kibana audit log file +export function registerCreateEnrollmentTokensRoute(server) { + const DEFAULT_NUM_TOKENS = 1; + const enrollmentTokensTtlInSeconds = server.config().get('xpack.beats.enrollmentTokensTtlInSeconds'); + + server.route({ + method: 'POST', + path: '/api/beats/enrollment_tokens', + config: { + validate: { + payload: Joi.object({ + num_tokens: Joi.number().optional().default(DEFAULT_NUM_TOKENS).min(1) + }).allow(null) + } + }, + handler: async (request, reply) => { + const callWithRequest = callWithRequestFactory(server, request); + const numTokens = get(request, 'payload.num_tokens', DEFAULT_NUM_TOKENS); + + const tokens = []; + while (tokens.length < numTokens) { + tokens.push(uuid.v4().replace(/-/g, "")); + } + + try { + await persistTokens(callWithRequest, tokens, enrollmentTokensTtlInSeconds); + } catch (err) { + return reply(wrapEsError(err)); + } + + const response = { tokens }; + reply(response); + } + }); +} diff --git a/x-pack/test/api_integration/apis/beats/create_enrollment_token.js b/x-pack/test/api_integration/apis/beats/create_enrollment_token.js new file mode 100644 index 0000000000000..6b446c7bf40e1 --- /dev/null +++ b/x-pack/test/api_integration/apis/beats/create_enrollment_token.js @@ -0,0 +1,109 @@ +/* + * 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'; + +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_token', () => { + const cleanup = () => { + return es.indices.delete({ + index: ES_INDEX_NAME, + ignore: [ 404 ] + }); + }; + + beforeEach(cleanup); + afterEach(cleanup); + + it('should create one token by default', async () => { + const { body: apiResponse } = await supertest + .post( + '/api/beats/enrollment_tokens' + ) + .set('kbn-xsrf', 'xxx') + .send() + .expect(200); + + const tokensFromApi = apiResponse.tokens; + + const esResponse = await es.search({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + q: 'type:enrollment_token' + }); + + const tokensInEs = esResponse.hits.hits + .map(hit => hit._source.enrollment_token.token); + + expect(tokensFromApi.length).to.eql(1); + expect(tokensFromApi).to.eql(tokensInEs); + }); + + it('should create the specified number of tokens', async () => { + const numTokens = chance.integer({ min: 1, max: 2000 }); + + const { body: apiResponse } = await supertest + .post( + '/api/beats/enrollment_tokens' + ) + .set('kbn-xsrf', 'xxx') + .send({ + num_tokens: numTokens + }) + .expect(200); + + const tokensFromApi = apiResponse.tokens; + + const esResponse = await es.search({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + q: 'type:enrollment_token', + size: numTokens + }); + + const tokensInEs = esResponse.hits.hits + .map(hit => hit._source.enrollment_token.token); + + expect(tokensFromApi.length).to.eql(numTokens); + expect(tokensFromApi).to.eql(tokensInEs); + }); + + it('should set token expiration to 10 minutes from now by default', async () => { + await supertest + .post( + '/api/beats/enrollment_tokens' + ) + .set('kbn-xsrf', 'xxx') + .send() + .expect(200); + + const esResponse = await es.search({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + q: 'type:enrollment_token' + }); + + const tokenInEs = esResponse.hits.hits[0]._source.enrollment_token; + + // We do a fuzzy check to see if the token expires between 9 and 10 minutes + // from now because a bit of time has elapsed been the creation of the + // tokens and this check. + const tokenExpiresOn = moment(tokenInEs.expires_on).valueOf(); + const tenMinutesFromNow = moment().add('10', 'minutes').valueOf(); + const almostTenMinutesFromNow = moment(tenMinutesFromNow).subtract('2', 'seconds').valueOf(); + expect(tokenExpiresOn).to.be.lessThan(tenMinutesFromNow); + expect(tokenExpiresOn).to.be.greaterThan(almostTenMinutesFromNow); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/beats/index.js b/x-pack/test/api_integration/apis/beats/index.js new file mode 100644 index 0000000000000..570393383bb8d --- /dev/null +++ b/x-pack/test/api_integration/apis/beats/index.js @@ -0,0 +1,11 @@ +/* + * 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 default function ({ loadTestFile }) { + describe('beats', () => { + loadTestFile(require.resolve('./create_enrollment_token')); + }); +} diff --git a/x-pack/test/api_integration/apis/index.js b/x-pack/test/api_integration/apis/index.js index 7f105650141d9..a6a252ac4efdd 100644 --- a/x-pack/test/api_integration/apis/index.js +++ b/x-pack/test/api_integration/apis/index.js @@ -11,5 +11,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./xpack_main')); loadTestFile(require.resolve('./logstash')); loadTestFile(require.resolve('./kibana')); + loadTestFile(require.resolve('./beats')); }); } diff --git a/x-pack/test/api_integration/config.js b/x-pack/test/api_integration/config.js index 3368562972cb8..880a42d6485fc 100644 --- a/x-pack/test/api_integration/config.js +++ b/x-pack/test/api_integration/config.js @@ -29,6 +29,7 @@ export default async function ({ readConfigFile }) { esArchiver: kibanaCommonConfig.get('services.esArchiver'), usageAPI: UsageAPIProvider, kibanaServer: kibanaCommonConfig.get('services.kibanaServer'), + chance: kibanaAPITestsConfig.get('services.chance'), }, esArchiver: xPackFunctionalTestsConfig.get('esArchiver'), junit: { From 7468f999b26d5029ba8bdf746bbd4a09478bda8d Mon Sep 17 00:00:00 2001 From: Shaunak Kashyap Date: Wed, 16 May 2018 05:59:41 -0700 Subject: [PATCH 06/43] Fixing name of test file (#19100) --- .../{create_enrollment_token.js => create_enrollment_tokens.js} | 2 +- x-pack/test/api_integration/apis/beats/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename x-pack/test/api_integration/apis/beats/{create_enrollment_token.js => create_enrollment_tokens.js} (98%) diff --git a/x-pack/test/api_integration/apis/beats/create_enrollment_token.js b/x-pack/test/api_integration/apis/beats/create_enrollment_tokens.js similarity index 98% rename from x-pack/test/api_integration/apis/beats/create_enrollment_token.js rename to x-pack/test/api_integration/apis/beats/create_enrollment_tokens.js index 6b446c7bf40e1..a12849d7a1c34 100644 --- a/x-pack/test/api_integration/apis/beats/create_enrollment_token.js +++ b/x-pack/test/api_integration/apis/beats/create_enrollment_tokens.js @@ -15,7 +15,7 @@ export default function ({ getService }) { const ES_INDEX_NAME = '.management-beats'; const ES_TYPE_NAME = '_doc'; - describe('create_enrollment_token', () => { + describe('create_enrollment_tokens', () => { const cleanup = () => { return es.indices.delete({ index: ES_INDEX_NAME, diff --git a/x-pack/test/api_integration/apis/beats/index.js b/x-pack/test/api_integration/apis/beats/index.js index 570393383bb8d..f8344895f02aa 100644 --- a/x-pack/test/api_integration/apis/beats/index.js +++ b/x-pack/test/api_integration/apis/beats/index.js @@ -6,6 +6,6 @@ export default function ({ loadTestFile }) { describe('beats', () => { - loadTestFile(require.resolve('./create_enrollment_token')); + loadTestFile(require.resolve('./create_enrollment_tokens')); }); } From 4193a514ff7414859c1b9757c95f6a54345befe7 Mon Sep 17 00:00:00 2001 From: Shaunak Kashyap Date: Wed, 16 May 2018 08:31:57 -0700 Subject: [PATCH 07/43] [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 --- .../call_with_request_factory.js | 5 +- .../plugins/beats/server/lib/client/index.js | 1 + .../lib/index_template/beats_template.json | 5 +- .../plugins/beats/server/routes/api/index.js | 2 + ...register_create_enrollment_tokens_route.js | 2 +- .../routes/api/register_enroll_beat_route.js | 104 ++++++++++ .../api_integration/apis/beats/constants.js} | 4 +- .../apis/beats/create_enrollment_tokens.js | 19 +- .../api_integration/apis/beats/enroll_beat.js | 183 ++++++++++++++++++ .../test/api_integration/apis/beats/index.js | 14 +- 10 files changed, 315 insertions(+), 24 deletions(-) rename x-pack/plugins/beats/server/lib/{call_with_request_factory => client}/call_with_request_factory.js (73%) create mode 100644 x-pack/plugins/beats/server/routes/api/register_enroll_beat_route.js rename x-pack/{plugins/beats/server/lib/call_with_request_factory/index.js => test/api_integration/apis/beats/constants.js} (73%) create mode 100644 x-pack/test/api_integration/apis/beats/enroll_beat.js diff --git a/x-pack/plugins/beats/server/lib/call_with_request_factory/call_with_request_factory.js b/x-pack/plugins/beats/server/lib/client/call_with_request_factory.js similarity index 73% rename from x-pack/plugins/beats/server/lib/call_with_request_factory/call_with_request_factory.js rename to x-pack/plugins/beats/server/lib/client/call_with_request_factory.js index 0c4f909d12f61..c81670ed0cdec 100644 --- a/x-pack/plugins/beats/server/lib/call_with_request_factory/call_with_request_factory.js +++ b/x-pack/plugins/beats/server/lib/client/call_with_request_factory.js @@ -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) => { diff --git a/x-pack/plugins/beats/server/lib/client/index.js b/x-pack/plugins/beats/server/lib/client/index.js index a56a50e2864a5..cdeee091cc66f 100644 --- a/x-pack/plugins/beats/server/lib/client/index.js +++ b/x-pack/plugins/beats/server/lib/client/index.js @@ -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'; diff --git a/x-pack/plugins/beats/server/lib/index_template/beats_template.json b/x-pack/plugins/beats/server/lib/index_template/beats_template.json index 6ef57b9c549ed..9b37b7e816bf8 100644 --- a/x-pack/plugins/beats/server/lib/index_template/beats_template.json +++ b/x-pack/plugins/beats/server/lib/index_template/beats_template.json @@ -45,9 +45,6 @@ "id": { "type": "keyword" }, - "enrollment_token": { - "type": "keyword" - }, "access_token": { "type": "keyword" }, @@ -58,7 +55,7 @@ "type": "keyword" }, "host_ip": { - "type": "keyword" + "type": "ip" }, "host_name": { "type": "keyword" diff --git a/x-pack/plugins/beats/server/routes/api/index.js b/x-pack/plugins/beats/server/routes/api/index.js index 8bf546045fe40..07d923876ee79 100644 --- a/x-pack/plugins/beats/server/routes/api/index.js +++ b/x-pack/plugins/beats/server/routes/api/index.js @@ -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); } diff --git a/x-pack/plugins/beats/server/routes/api/register_create_enrollment_tokens_route.js b/x-pack/plugins/beats/server/routes/api/register_create_enrollment_tokens_route.js index 582ae59062d8b..87ae30cd0e532 100644 --- a/x-pack/plugins/beats/server/routes/api/register_create_enrollment_tokens_route.js +++ b/x-pack/plugins/beats/server/routes/api/register_create_enrollment_tokens_route.js @@ -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) { diff --git a/x-pack/plugins/beats/server/routes/api/register_enroll_beat_route.js b/x-pack/plugins/beats/server/routes/api/register_enroll_beat_route.js new file mode 100644 index 0000000000000..fb004fbb79e12 --- /dev/null +++ b/x-pack/plugins/beats/server/routes/api/register_enroll_beat_route.js @@ -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); + } + }); +} diff --git a/x-pack/plugins/beats/server/lib/call_with_request_factory/index.js b/x-pack/test/api_integration/apis/beats/constants.js similarity index 73% rename from x-pack/plugins/beats/server/lib/call_with_request_factory/index.js rename to x-pack/test/api_integration/apis/beats/constants.js index 787814d87dff9..00327aface171 100644 --- a/x-pack/plugins/beats/server/lib/call_with_request_factory/index.js +++ b/x-pack/test/api_integration/apis/beats/constants.js @@ -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'; + diff --git a/x-pack/test/api_integration/apis/beats/create_enrollment_tokens.js b/x-pack/test/api_integration/apis/beats/create_enrollment_tokens.js index a12849d7a1c34..86b80323773b4 100644 --- a/x-pack/test/api_integration/apis/beats/create_enrollment_tokens.js +++ b/x-pack/test/api_integration/apis/beats/create_enrollment_tokens.js @@ -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( diff --git a/x-pack/test/api_integration/apis/beats/enroll_beat.js b/x-pack/test/api_integration/apis/beats/enroll_beat.js new file mode 100644 index 0000000000000..ec3785f8eb35d --- /dev/null +++ b/x-pack/test/api_integration/apis/beats/enroll_beat.js @@ -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); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/beats/index.js b/x-pack/test/api_integration/apis/beats/index.js index f8344895f02aa..dc6137f979019 100644 --- a/x-pack/test/api_integration/apis/beats/index.js +++ b/x-pack/test/api_integration/apis/beats/index.js @@ -4,8 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -export default function ({ loadTestFile }) { +import { ES_INDEX_NAME } from './constants'; + +export default function ({ getService, loadTestFile }) { + const es = getService('es'); + describe('beats', () => { + const cleanup = () => es.indices.delete({ + index: ES_INDEX_NAME, + ignore: [ 404 ] + }); + + beforeEach(cleanup); + loadTestFile(require.resolve('./create_enrollment_tokens')); + loadTestFile(require.resolve('./enroll_beat')); }); } From 29c98766cf17544fbe210bfd75d051c0067e14b9 Mon Sep 17 00:00:00 2001 From: Shaunak Kashyap Date: Wed, 16 May 2018 13:40:39 -0700 Subject: [PATCH 08/43] [Beats Management] APIs: List beats (#19086) * WIP checkin * Add API integration test * Converting to Jest test * WIP checkin * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Updating mapping --- .../plugins/beats/server/routes/api/index.js | 2 + .../routes/api/register_list_beats_route.js | 47 ++++++++++ .../test/api_integration/apis/beats/index.js | 1 + .../api_integration/apis/beats/list_beats.js | 46 ++++++++++ .../es_archives/beats/list/data.json.gz | Bin 0 -> 343 bytes .../es_archives/beats/list/mappings.json | 82 ++++++++++++++++++ 6 files changed, 178 insertions(+) create mode 100644 x-pack/plugins/beats/server/routes/api/register_list_beats_route.js create mode 100644 x-pack/test/api_integration/apis/beats/list_beats.js create mode 100644 x-pack/test/functional/es_archives/beats/list/data.json.gz create mode 100644 x-pack/test/functional/es_archives/beats/list/mappings.json diff --git a/x-pack/plugins/beats/server/routes/api/index.js b/x-pack/plugins/beats/server/routes/api/index.js index 07d923876ee79..76cedde5cdf3d 100644 --- a/x-pack/plugins/beats/server/routes/api/index.js +++ b/x-pack/plugins/beats/server/routes/api/index.js @@ -6,8 +6,10 @@ import { registerCreateEnrollmentTokensRoute } from './register_create_enrollment_tokens_route'; import { registerEnrollBeatRoute } from './register_enroll_beat_route'; +import { registerListBeatsRoute } from './register_list_beats_route'; export function registerApiRoutes(server) { registerCreateEnrollmentTokensRoute(server); registerEnrollBeatRoute(server); + registerListBeatsRoute(server); } diff --git a/x-pack/plugins/beats/server/routes/api/register_list_beats_route.js b/x-pack/plugins/beats/server/routes/api/register_list_beats_route.js new file mode 100644 index 0000000000000..b84210988978f --- /dev/null +++ b/x-pack/plugins/beats/server/routes/api/register_list_beats_route.js @@ -0,0 +1,47 @@ +/* + * 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 { + get, + omit +} from "lodash"; +import { INDEX_NAMES } from "../../../common/constants"; +import { callWithRequestFactory } from '../../lib/client'; +import { wrapEsError } from "../../lib/error_wrappers"; + +async function getBeats(callWithRequest) { + const params = { + index: INDEX_NAMES.BEATS, + type: '_doc', + q: 'type:beat' + }; + + const response = await callWithRequest('search', params); + return get(response, 'hits.hits', []); +} + +// TODO: add license check pre-hook +export function registerListBeatsRoute(server) { + server.route({ + method: 'GET', + path: '/api/beats/agents', + handler: async (request, reply) => { + const callWithRequest = callWithRequestFactory(server, request); + let beats; + + try { + beats = await getBeats(callWithRequest); + } catch (err) { + return reply(wrapEsError(err)); + } + + const response = { + beats: beats.map(beat => omit(beat._source.beat, ['access_token'])) + }; + reply(response); + } + }); +} diff --git a/x-pack/test/api_integration/apis/beats/index.js b/x-pack/test/api_integration/apis/beats/index.js index dc6137f979019..6b3562863a2b7 100644 --- a/x-pack/test/api_integration/apis/beats/index.js +++ b/x-pack/test/api_integration/apis/beats/index.js @@ -19,5 +19,6 @@ export default function ({ getService, loadTestFile }) { loadTestFile(require.resolve('./create_enrollment_tokens')); loadTestFile(require.resolve('./enroll_beat')); + loadTestFile(require.resolve('./list_beats')); }); } diff --git a/x-pack/test/api_integration/apis/beats/list_beats.js b/x-pack/test/api_integration/apis/beats/list_beats.js new file mode 100644 index 0000000000000..dfd0dccf32cc0 --- /dev/null +++ b/x-pack/test/api_integration/apis/beats/list_beats.js @@ -0,0 +1,46 @@ +/* + * 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'; + +export default function ({ getService }) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('list_beats', () => { + const archive = 'beats/list'; + + beforeEach('load beats archive', () => esArchiver.load(archive)); + afterEach('unload beats archive', () => esArchiver.unload(archive)); + + it('should return all beats', async () => { + const { body: apiResponse } = await supertest + .get( + '/api/beats/agents' + ) + .expect(200); + + const beatsFromApi = apiResponse.beats; + + expect(beatsFromApi.length).to.be(3); + expect(beatsFromApi.filter(beat => beat.hasOwnProperty('verified_on')).length).to.be(1); + expect(beatsFromApi.find(beat => beat.hasOwnProperty('verified_on')).id).to.be('foo'); + }); + + it('should not return access tokens', async () => { + const { body: apiResponse } = await supertest + .get( + '/api/beats/agents' + ) + .expect(200); + + const beatsFromApi = apiResponse.beats; + + expect(beatsFromApi.length).to.be(3); + expect(beatsFromApi.filter(beat => beat.hasOwnProperty('access_token')).length).to.be(0); + }); + }); +} diff --git a/x-pack/test/functional/es_archives/beats/list/data.json.gz b/x-pack/test/functional/es_archives/beats/list/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..c5bcfc6fb14f91eaed3dd09d5fc8ec099ff637a3 GIT binary patch literal 343 zcmV-d0jT~TiwFqyEc;pj17u-zVJ>QOZ*Bm^Qp;|GFc7@+6^L_V*QP zHQ*FTls3x07v~|UN_uIPUM%fAR-^GAqBu^5_YEdRoH%cjhXCwgy$#4=9LBM39qxmG zG|<8`HrNg;gD~_b`D{aZT@hR^AVF5VZTDBS_uI}+yJy~@yr|;KG^u8~s$Sz4?a00O zekkirpczR?M))_jh30Jco*3we_03#!PCErXfnY86eL477Yy+)9TCD+T7VT>oK~%$LJVEhr5();N$N~ZgA*o`$Ns?*m6b~Bm8#NW1`ztPjMHkW=f?( zpb?S+=UkaQl|ov9T3bL_{cF|Z4c)QoUtRPRb@`$*%Yi#bm5|&rr6EVlkyn&UqjNF$ p?y#$?8eQp6)|4`}qGH9wBa=lcicArm@~7pW`2>YU+a_rQ0061trn&$C literal 0 HcmV?d00001 diff --git a/x-pack/test/functional/es_archives/beats/list/mappings.json b/x-pack/test/functional/es_archives/beats/list/mappings.json new file mode 100644 index 0000000000000..92d89fb159733 --- /dev/null +++ b/x-pack/test/functional/es_archives/beats/list/mappings.json @@ -0,0 +1,82 @@ +{ + "type": "index", + "value": { + "index": ".management-beats", + "settings": { + "index": { + "codec": "best_compression", + "number_of_shards": "1", + "auto_expand_replicas": "0-1", + "number_of_replicas": "0" + } + }, + "mappings": { + "_doc": { + "dynamic": "strict", + "properties": { + "type": { + "type": "keyword" + }, + "enrollment_token": { + "properties": { + "token": { + "type": "keyword" + }, + "expires_on": { + "type": "date" + } + } + }, + "configuration_block": { + "properties": { + "tag": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "block_yml": { + "type": "text" + } + } + }, + "beat": { + "properties": { + "id": { + "type": "keyword" + }, + "access_token": { + "type": "keyword" + }, + "verified_on": { + "type": "date" + }, + "type": { + "type": "keyword" + }, + "host_ip": { + "type": "ip" + }, + "host_name": { + "type": "keyword" + }, + "ephemeral_id": { + "type": "keyword" + }, + "local_configuration_yml": { + "type": "text" + }, + "central_configuration_yml": { + "type": "text" + }, + "metadata": { + "dynamic": "true", + "type": "object" + } + } + } + } + } + } + } +} From 88a0e5634030f13787639fc71e969ae9b2a3540b Mon Sep 17 00:00:00 2001 From: Shaunak Kashyap Date: Thu, 17 May 2018 09:18:48 -0700 Subject: [PATCH 09/43] [Beats Management] APIs: Verify beats (#19103) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Fleshing out remaining tests * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Moving TODO comment to right file * Rename determine* helper functions to find* --- .../plugins/beats/server/routes/api/index.js | 2 + .../routes/api/register_verify_beats_route.js | 143 ++++++++++++++++++ .../test/api_integration/apis/beats/index.js | 1 + .../apis/beats/verify_beats.js | 81 ++++++++++ .../es_archives/beats/list/data.json.gz | Bin 343 -> 371 bytes 5 files changed, 227 insertions(+) create mode 100644 x-pack/plugins/beats/server/routes/api/register_verify_beats_route.js create mode 100644 x-pack/test/api_integration/apis/beats/verify_beats.js diff --git a/x-pack/plugins/beats/server/routes/api/index.js b/x-pack/plugins/beats/server/routes/api/index.js index 76cedde5cdf3d..def322f0e94eb 100644 --- a/x-pack/plugins/beats/server/routes/api/index.js +++ b/x-pack/plugins/beats/server/routes/api/index.js @@ -7,9 +7,11 @@ import { registerCreateEnrollmentTokensRoute } from './register_create_enrollment_tokens_route'; import { registerEnrollBeatRoute } from './register_enroll_beat_route'; import { registerListBeatsRoute } from './register_list_beats_route'; +import { registerVerifyBeatsRoute } from './register_verify_beats_route'; export function registerApiRoutes(server) { registerCreateEnrollmentTokensRoute(server); registerEnrollBeatRoute(server); registerListBeatsRoute(server); + registerVerifyBeatsRoute(server); } diff --git a/x-pack/plugins/beats/server/routes/api/register_verify_beats_route.js b/x-pack/plugins/beats/server/routes/api/register_verify_beats_route.js new file mode 100644 index 0000000000000..11a4aff1204dc --- /dev/null +++ b/x-pack/plugins/beats/server/routes/api/register_verify_beats_route.js @@ -0,0 +1,143 @@ +/* + * 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 moment from 'moment'; +import { + get, + flatten +} from 'lodash'; +import { INDEX_NAMES } from '../../../common/constants'; +import { callWithRequestFactory } from '../../lib/client'; +import { wrapEsError } from '../../lib/error_wrappers'; + +async function getBeats(callWithRequest, beatIds) { + const ids = beatIds.map(beatId => `beat:${beatId}`); + const params = { + index: INDEX_NAMES.BEATS, + type: '_doc', + body: { ids }, + _sourceInclude: [ 'beat.id', 'beat.verified_on' ] + }; + + const response = await callWithRequest('mget', params); + return get(response, 'docs', []); +} + +async function verifyBeats(callWithRequest, beatIds) { + if (!Array.isArray(beatIds) || (beatIds.length === 0)) { + return []; + } + + const verifiedOn = moment().toJSON(); + const body = flatten(beatIds.map(beatId => [ + { update: { _id: `beat:${beatId}` } }, + { doc: { beat: { verified_on: verifiedOn } } } + ])); + + const params = { + index: INDEX_NAMES.BEATS, + type: '_doc', + body, + refresh: 'wait_for' + }; + + const response = await callWithRequest('bulk', params); + return get(response, 'items', []); +} + +function findNonExistentBeatIds(beatsFromEs, beatIdsFromRequest) { + return beatsFromEs.reduce((nonExistentBeatIds, beatFromEs, idx) => { + if (!beatFromEs.found) { + nonExistentBeatIds.push(beatIdsFromRequest[idx]); + } + return nonExistentBeatIds; + }, []); +} + +function findAlreadyVerifiedBeatIds(beatsFromEs) { + return beatsFromEs + .filter(beat => beat.found) + .filter(beat => beat._source.beat.hasOwnProperty('verified_on')) + .map(beat => beat._source.beat.id); +} + +function findToBeVerifiedBeatIds(beatsFromEs) { + return beatsFromEs + .filter(beat => beat.found) + .filter(beat => !beat._source.beat.hasOwnProperty('verified_on')) + .map(beat => beat._source.beat.id); +} + +function findVerifiedBeatIds(verifications, toBeVerifiedBeatIds) { + return verifications.reduce((verifiedBeatIds, verification, idx) => { + if (verification.update.status === 200) { + verifiedBeatIds.push(toBeVerifiedBeatIds[idx]); + } + return verifiedBeatIds; + }, []); +} + +// TODO: add license check pre-hook +// TODO: write to Kibana audit log file (include who did the verification as well) +export function registerVerifyBeatsRoute(server) { + server.route({ + method: 'POST', + path: '/api/beats/agents/verify', + config: { + validate: { + payload: Joi.object({ + beats: Joi.array({ + id: Joi.string().required() + }).min(1) + }).required() + } + }, + handler: async (request, reply) => { + const callWithRequest = callWithRequestFactory(server, request); + + const beats = [...request.payload.beats]; + const beatIds = beats.map(beat => beat.id); + + let nonExistentBeatIds; + let alreadyVerifiedBeatIds; + let verifiedBeatIds; + + try { + const beatsFromEs = await getBeats(callWithRequest, beatIds); + + nonExistentBeatIds = findNonExistentBeatIds(beatsFromEs, beatIds); + alreadyVerifiedBeatIds = findAlreadyVerifiedBeatIds(beatsFromEs); + const toBeVerifiedBeatIds = findToBeVerifiedBeatIds(beatsFromEs); + + const verifications = await verifyBeats(callWithRequest, toBeVerifiedBeatIds); + verifiedBeatIds = findVerifiedBeatIds(verifications, toBeVerifiedBeatIds); + + } catch (err) { + return reply(wrapEsError(err)); + } + + beats.forEach(beat => { + if (nonExistentBeatIds.includes(beat.id)) { + beat.status = 404; + beat.result = 'not found'; + } else if (alreadyVerifiedBeatIds.includes(beat.id)) { + beat.status = 200; + beat.result = 'already verified'; + } else if (verifiedBeatIds.includes(beat.id)) { + beat.status = 200; + beat.result = 'verified'; + } else { + beat.status = 400; + beat.result = 'not verified'; + } + }); + + const response = { beats }; + reply(response); + } + }); +} diff --git a/x-pack/test/api_integration/apis/beats/index.js b/x-pack/test/api_integration/apis/beats/index.js index 6b3562863a2b7..abb97b3daed91 100644 --- a/x-pack/test/api_integration/apis/beats/index.js +++ b/x-pack/test/api_integration/apis/beats/index.js @@ -20,5 +20,6 @@ export default function ({ getService, loadTestFile }) { loadTestFile(require.resolve('./create_enrollment_tokens')); loadTestFile(require.resolve('./enroll_beat')); loadTestFile(require.resolve('./list_beats')); + loadTestFile(require.resolve('./verify_beats')); }); } diff --git a/x-pack/test/api_integration/apis/beats/verify_beats.js b/x-pack/test/api_integration/apis/beats/verify_beats.js new file mode 100644 index 0000000000000..2b085308b43d1 --- /dev/null +++ b/x-pack/test/api_integration/apis/beats/verify_beats.js @@ -0,0 +1,81 @@ +/* + * 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'; + +export default function ({ getService }) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const chance = getService('chance'); + + describe('verify_beats', () => { + const archive = 'beats/list'; + + beforeEach('load beats archive', () => esArchiver.load(archive)); + afterEach('unload beats archive', () => esArchiver.unload(archive)); + + it('verify the given beats', async () => { + const { body: apiResponse } = await supertest + .post( + '/api/beats/agents/verify' + ) + .set('kbn-xsrf', 'xxx') + .send({ + beats: [ + { id: 'bar' }, + { id: 'baz' } + ] + }) + .expect(200); + + expect(apiResponse.beats).to.eql([ + { id: 'bar', status: 200, result: 'verified' }, + { id: 'baz', status: 200, result: 'verified' }, + ]); + }); + + it('should not re-verify already-verified beats', async () => { + const { body: apiResponse } = await supertest + .post( + '/api/beats/agents/verify' + ) + .set('kbn-xsrf', 'xxx') + .send({ + beats: [ + { id: 'foo' }, + { id: 'bar' } + ] + }) + .expect(200); + + expect(apiResponse.beats).to.eql([ + { id: 'foo', status: 200, result: 'already verified' }, + { id: 'bar', status: 200, result: 'verified' } + ]); + }); + + it('should return errors for non-existent beats', async () => { + const nonExistentBeatId = chance.word(); + const { body: apiResponse } = await supertest + .post( + '/api/beats/agents/verify' + ) + .set('kbn-xsrf', 'xxx') + .send({ + beats: [ + { id: 'bar' }, + { id: nonExistentBeatId } + ] + }) + .expect(200); + + expect(apiResponse.beats).to.eql([ + { id: 'bar', status: 200, result: 'verified' }, + { id: nonExistentBeatId, status: 404, result: 'not found' }, + ]); + }); + }); +} diff --git a/x-pack/test/functional/es_archives/beats/list/data.json.gz b/x-pack/test/functional/es_archives/beats/list/data.json.gz index c5bcfc6fb14f91eaed3dd09d5fc8ec099ff637a3..f3ccd2687455692022cc5fb6622b906cd0ec6293 100644 GIT binary patch literal 371 zcmV-(0gV11iwFp7ua17u-zVJ>QOZ*Bm^RKae8Fbuu(6^Qd1C6J^E-?7s!$VtqG zHb@7w>Q?pNM`$aU)^>+Y?In`!_pGO9JG&^3lm26cNggN8+vFi6Ht@C%ncWZ!VbwU? z1^}s{foH6-=@$l}??(8nLvd;mST1A&EPr2bPub3|TRZihaRc&*ijUERn&Hao4ZmTB z+Kcb{qFRMABPq!U|50tAKG3}<23lf$J;xl>PC~~dSc_d(^!^o_P}U%=)_}BhiW@4G zVtPp#liE53+$2ZpK03YoXdgwpo0x3i^Z!h)v2QDT#pZNyIU|e_e%b0l(PgVAxo53r zN~J=e5t0JuT$yDmg|q^-wt%v{tJT8}-O%bkZS*Ad{6=S%19y%pA-QEr!xAk=UQ#ZN zUWz$)gKbq-=n6klQ_9qWiUkvoOy;S`GevaDpYD7F?d^V=VKCzrTseU-n+xmTUYpA= RW6|eL{sI(FHM`CQ003wLvN`|& literal 343 zcmV-d0jT~TiwFqyEc;pj17u-zVJ>QOZ*Bm^Qp;|GFc7@+6^L_V*QP zHQ*FTls3x07v~|UN_uIPUM%fAR-^GAqBu^5_YEdRoH%cjhXCwgy$#4=9LBM39qxmG zG|<8`HrNg;gD~_b`D{aZT@hR^AVF5VZTDBS_uI}+yJy~@yr|;KG^u8~s$Sz4?a00O zekkirpczR?M))_jh30Jco*3we_03#!PCErXfnY86eL477Yy+)9TCD+T7VT>oK~%$LJVEhr5();N$N~ZgA*o`$Ns?*m6b~Bm8#NW1`ztPjMHkW=f?( zpb?S+=UkaQl|ov9T3bL_{cF|Z4c)QoUtRPRb@`$*%Yi#bm5|&rr6EVlkyn&UqjNF$ p?y#$?8eQp6)|4`}qGH9wBa=lcicArm@~7pW`2>YU+a_rQ0061trn&$C From 75cf00470194717b081908054a631258eb919047 Mon Sep 17 00:00:00 2001 From: Shaunak Kashyap Date: Fri, 18 May 2018 07:51:55 -0700 Subject: [PATCH 10/43] Fixing assertions (#19194) --- x-pack/test/api_integration/apis/beats/list_beats.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/test/api_integration/apis/beats/list_beats.js b/x-pack/test/api_integration/apis/beats/list_beats.js index dfd0dccf32cc0..c8e1c754b622c 100644 --- a/x-pack/test/api_integration/apis/beats/list_beats.js +++ b/x-pack/test/api_integration/apis/beats/list_beats.js @@ -25,7 +25,7 @@ export default function ({ getService }) { const beatsFromApi = apiResponse.beats; - expect(beatsFromApi.length).to.be(3); + expect(beatsFromApi.length).to.be(4); expect(beatsFromApi.filter(beat => beat.hasOwnProperty('verified_on')).length).to.be(1); expect(beatsFromApi.find(beat => beat.hasOwnProperty('verified_on')).id).to.be('foo'); }); @@ -39,7 +39,7 @@ export default function ({ getService }) { const beatsFromApi = apiResponse.beats; - expect(beatsFromApi.length).to.be(3); + expect(beatsFromApi.length).to.be(4); expect(beatsFromApi.filter(beat => beat.hasOwnProperty('access_token')).length).to.be(0); }); }); From 3d8ca8aa37ce53929fd03472ca4442998b14f7c4 Mon Sep 17 00:00:00 2001 From: Shaunak Kashyap Date: Mon, 21 May 2018 05:32:18 -0700 Subject: [PATCH 11/43] [Beats Management] APIs: Update beat (#19148) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Add API tests * Update template to allow version field for beat * Implement PUT /api/beats/agent/{beat ID} API * Make enroll beat code consistent with update beat code * Fixing minor typo in TODO comment * Allow version in request payload * Make sure beat is not updated in ES in error scenarios * Adding version as required field in Enroll Beat API payload * Using destructuring * Fixing rename that was accidentally reversed in conflict fixing --- .../lib/index_template/beats_template.json | 3 + .../plugins/beats/server/routes/api/index.js | 2 + .../routes/api/register_enroll_beat_route.js | 20 +-- .../routes/api/register_update_beat_route.js | 98 +++++++++++++ .../routes/api/register_verify_beats_route.js | 2 +- .../api_integration/apis/beats/enroll_beat.js | 7 + .../test/api_integration/apis/beats/index.js | 1 + .../api_integration/apis/beats/update_beat.js | 130 ++++++++++++++++++ .../es_archives/beats/list/mappings.json | 3 + 9 files changed, 256 insertions(+), 10 deletions(-) create mode 100644 x-pack/plugins/beats/server/routes/api/register_update_beat_route.js create mode 100644 x-pack/test/api_integration/apis/beats/update_beat.js diff --git a/x-pack/plugins/beats/server/lib/index_template/beats_template.json b/x-pack/plugins/beats/server/lib/index_template/beats_template.json index 9b37b7e816bf8..e293845f9a3a8 100644 --- a/x-pack/plugins/beats/server/lib/index_template/beats_template.json +++ b/x-pack/plugins/beats/server/lib/index_template/beats_template.json @@ -54,6 +54,9 @@ "type": { "type": "keyword" }, + "version": { + "type": "keyword" + }, "host_ip": { "type": "ip" }, diff --git a/x-pack/plugins/beats/server/routes/api/index.js b/x-pack/plugins/beats/server/routes/api/index.js index def322f0e94eb..4e6ee318668bf 100644 --- a/x-pack/plugins/beats/server/routes/api/index.js +++ b/x-pack/plugins/beats/server/routes/api/index.js @@ -8,10 +8,12 @@ import { registerCreateEnrollmentTokensRoute } from './register_create_enrollmen 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'; export function registerApiRoutes(server) { registerCreateEnrollmentTokensRoute(server); registerEnrollBeatRoute(server); registerListBeatsRoute(server); registerVerifyBeatsRoute(server); + registerUpdateBeatRoute(server); } diff --git a/x-pack/plugins/beats/server/routes/api/register_enroll_beat_route.js b/x-pack/plugins/beats/server/routes/api/register_enroll_beat_route.js index fb004fbb79e12..2a86f33b0d28f 100644 --- a/x-pack/plugins/beats/server/routes/api/register_enroll_beat_route.js +++ b/x-pack/plugins/beats/server/routes/api/register_enroll_beat_route.js @@ -37,21 +37,16 @@ function deleteUsedEnrollmentToken(callWithInternalUser, enrollmentToken) { return callWithInternalUser('delete', params); } -function persistBeat(callWithInternalUser, beat, beatId, accessToken, remoteAddress) { +function persistBeat(callWithInternalUser, beat) { const body = { type: 'beat', - beat: { - ...omit(beat, 'enrollment_token'), - id: beatId, - access_token: accessToken, - host_ip: remoteAddress - } + beat }; const params = { index: INDEX_NAMES.BEATS, type: '_doc', - id: `beat:${beatId}`, + id: `beat:${beat.id}`, body, refresh: 'wait_for' }; @@ -69,6 +64,7 @@ export function registerEnrollBeatRoute(server) { payload: Joi.object({ enrollment_token: Joi.string().required(), type: Joi.string().required(), + version: Joi.string().required(), host_name: Joi.string().required() }).required() }, @@ -76,6 +72,7 @@ export function registerEnrollBeatRoute(server) { }, handler: async (request, reply) => { const callWithInternalUser = callWithInternalUserFactory(server); + const { beatId } = request.params; let accessToken; try { @@ -90,7 +87,12 @@ export function registerEnrollBeatRoute(server) { accessToken = uuid.v4().replace(/-/g, ""); const remoteAddress = request.info.remoteAddress; - await persistBeat(callWithInternalUser, request.payload, request.params.beatId, accessToken, remoteAddress); + await persistBeat(callWithInternalUser, { + ...omit(request.payload, 'enrollment_token'), + id: beatId, + access_token: accessToken, + host_ip: remoteAddress + }); await deleteUsedEnrollmentToken(callWithInternalUser, enrollmentToken); } catch (err) { diff --git a/x-pack/plugins/beats/server/routes/api/register_update_beat_route.js b/x-pack/plugins/beats/server/routes/api/register_update_beat_route.js new file mode 100644 index 0000000000000..c93eca7590454 --- /dev/null +++ b/x-pack/plugins/beats/server/routes/api/register_update_beat_route.js @@ -0,0 +1,98 @@ +/* + * 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 { get } from 'lodash'; +import { INDEX_NAMES } from '../../../common/constants'; +import { callWithInternalUserFactory } from '../../lib/client'; +import { wrapEsError } from '../../lib/error_wrappers'; + +async function getBeat(callWithInternalUser, beatId) { + const params = { + index: INDEX_NAMES.BEATS, + type: '_doc', + id: `beat:${beatId}`, + ignore: [ 404 ] + }; + + const response = await callWithInternalUser('get', params); + if (!response.found) { + return null; + } + + return get(response, '_source.beat'); +} + +function persistBeat(callWithInternalUser, beat) { + const body = { + type: 'beat', + beat + }; + + const params = { + index: INDEX_NAMES.BEATS, + type: '_doc', + id: `beat:${beat.id}`, + body, + refresh: 'wait_for' + }; + return callWithInternalUser('index', params); +} + +// TODO: add license check pre-hook +// TODO: write to Kibana audit log file (include who did the verification as well) +export function registerUpdateBeatRoute(server) { + server.route({ + method: 'PUT', + path: '/api/beats/agent/{beatId}', + config: { + validate: { + payload: Joi.object({ + access_token: Joi.string().required(), + type: Joi.string(), + version: Joi.string(), + host_name: Joi.string(), + ephemeral_id: Joi.string(), + local_configuration_yml: Joi.string(), + metadata: Joi.object() + }).required() + }, + auth: false + }, + handler: async (request, reply) => { + const callWithInternalUser = callWithInternalUserFactory(server); + const { beatId } = request.params; + + try { + const beat = await getBeat(callWithInternalUser, beatId); + if (beat === null) { + return reply({ message: 'Beat not found' }).code(404); + } + + const isAccessTokenValid = beat.access_token === request.payload.access_token; + if (!isAccessTokenValid) { + return reply({ message: 'Invalid access token' }).code(401); + } + + const isBeatVerified = beat.hasOwnProperty('verified_on'); + if (!isBeatVerified) { + return reply({ message: 'Beat has not been verified' }).code(400); + } + + const remoteAddress = request.info.remoteAddress; + await persistBeat(callWithInternalUser, { + ...beat, + ...request.payload, + host_ip: remoteAddress + }); + } catch (err) { + return reply(wrapEsError(err)); + } + + reply().code(204); + } + }); +} diff --git a/x-pack/plugins/beats/server/routes/api/register_verify_beats_route.js b/x-pack/plugins/beats/server/routes/api/register_verify_beats_route.js index 11a4aff1204dc..b2113029224a5 100644 --- a/x-pack/plugins/beats/server/routes/api/register_verify_beats_route.js +++ b/x-pack/plugins/beats/server/routes/api/register_verify_beats_route.js @@ -82,7 +82,7 @@ function findVerifiedBeatIds(verifications, toBeVerifiedBeatIds) { } // TODO: add license check pre-hook -// TODO: write to Kibana audit log file (include who did the verification as well) +// TODO: write to Kibana audit log file export function registerVerifyBeatsRoute(server) { server.route({ method: 'POST', diff --git a/x-pack/test/api_integration/apis/beats/enroll_beat.js b/x-pack/test/api_integration/apis/beats/enroll_beat.js index ec3785f8eb35d..388987f6d6d22 100644 --- a/x-pack/test/api_integration/apis/beats/enroll_beat.js +++ b/x-pack/test/api_integration/apis/beats/enroll_beat.js @@ -24,10 +24,17 @@ export default function ({ getService }) { beforeEach(async () => { validEnrollmentToken = chance.word(); beatId = chance.word(); + const version = chance.integer({ min: 1, max: 10 }) + + '.' + + chance.integer({ min: 1, max: 10 }) + + '.' + + chance.integer({ min: 1, max: 10 }); + beat = { enrollment_token: validEnrollmentToken, type: 'filebeat', host_name: 'foo.bar.com', + version }; await es.index({ diff --git a/x-pack/test/api_integration/apis/beats/index.js b/x-pack/test/api_integration/apis/beats/index.js index abb97b3daed91..b41f17ed749b3 100644 --- a/x-pack/test/api_integration/apis/beats/index.js +++ b/x-pack/test/api_integration/apis/beats/index.js @@ -21,5 +21,6 @@ export default function ({ getService, loadTestFile }) { loadTestFile(require.resolve('./enroll_beat')); loadTestFile(require.resolve('./list_beats')); loadTestFile(require.resolve('./verify_beats')); + loadTestFile(require.resolve('./update_beat')); }); } diff --git a/x-pack/test/api_integration/apis/beats/update_beat.js b/x-pack/test/api_integration/apis/beats/update_beat.js new file mode 100644 index 0000000000000..09d286be0fcf8 --- /dev/null +++ b/x-pack/test/api_integration/apis/beats/update_beat.js @@ -0,0 +1,130 @@ +/* + * 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 chance = getService('chance'); + const es = getService('es'); + const esArchiver = getService('esArchiver'); + + describe('update_beat', () => { + let beat; + const archive = 'beats/list'; + + beforeEach('load beats archive', () => esArchiver.load(archive)); + beforeEach(() => { + const version = chance.integer({ min: 1, max: 10 }) + + '.' + + chance.integer({ min: 1, max: 10 }) + + '.' + + chance.integer({ min: 1, max: 10 }); + + beat = { + access_token: '93c4a4dd08564c189a7ec4e4f046b975', + type: `${chance.word()}beat`, + host_name: `www.${chance.word()}.net`, + version, + ephemeral_id: chance.word() + }; + }); + + afterEach('unload beats archive', () => esArchiver.unload(archive)); + + it('should update an existing verified beat', async () => { + const beatId = 'foo'; + await supertest + .put( + `/api/beats/agent/${beatId}` + ) + .set('kbn-xsrf', 'xxx') + .send(beat) + .expect(204); + + const beatInEs = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:${beatId}` + }); + + expect(beatInEs._source.beat.id).to.be(beatId); + expect(beatInEs._source.beat.type).to.be(beat.type); + expect(beatInEs._source.beat.host_name).to.be(beat.host_name); + expect(beatInEs._source.beat.version).to.be(beat.version); + expect(beatInEs._source.beat.ephemeral_id).to.be(beat.ephemeral_id); + }); + + it('should return an error for an invalid access token', async () => { + const beatId = 'foo'; + beat.access_token = chance.word(); + const { body } = await supertest + .put( + `/api/beats/agent/${beatId}` + ) + .set('kbn-xsrf', 'xxx') + .send(beat) + .expect(401); + + expect(body.message).to.be('Invalid access token'); + + const beatInEs = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:${beatId}` + }); + + expect(beatInEs._source.beat.id).to.be(beatId); + expect(beatInEs._source.beat.type).to.not.be(beat.type); + expect(beatInEs._source.beat.host_name).to.not.be(beat.host_name); + expect(beatInEs._source.beat.version).to.not.be(beat.version); + expect(beatInEs._source.beat.ephemeral_id).to.not.be(beat.ephemeral_id); + }); + + it('should return an error for an existing but unverified beat', async () => { + const beatId = 'bar'; + beat.access_token = '3c4a4dd08564c189a7ec4e4f046b9759'; + const { body } = await supertest + .put( + `/api/beats/agent/${beatId}` + ) + .set('kbn-xsrf', 'xxx') + .send(beat) + .expect(400); + + expect(body.message).to.be('Beat has not been verified'); + + const beatInEs = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:${beatId}` + }); + + expect(beatInEs._source.beat.id).to.be(beatId); + expect(beatInEs._source.beat.type).to.not.be(beat.type); + expect(beatInEs._source.beat.host_name).to.not.be(beat.host_name); + expect(beatInEs._source.beat.version).to.not.be(beat.version); + expect(beatInEs._source.beat.ephemeral_id).to.not.be(beat.ephemeral_id); + }); + + it('should return an error for a non-existent beat', async () => { + const beatId = chance.word(); + const { body } = await supertest + .put( + `/api/beats/agent/${beatId}` + ) + .set('kbn-xsrf', 'xxx') + .send(beat) + .expect(404); + + expect(body.message).to.be('Beat not found'); + }); + }); +} diff --git a/x-pack/test/functional/es_archives/beats/list/mappings.json b/x-pack/test/functional/es_archives/beats/list/mappings.json index 92d89fb159733..24aceed8a5b61 100644 --- a/x-pack/test/functional/es_archives/beats/list/mappings.json +++ b/x-pack/test/functional/es_archives/beats/list/mappings.json @@ -54,6 +54,9 @@ "type": { "type": "keyword" }, + "version": { + "type": "keyword" + }, "host_ip": { "type": "ip" }, From 66eb24cb3637bdfe8a08da7b4115dce2a0556947 Mon Sep 17 00:00:00 2001 From: Shaunak Kashyap Date: Tue, 22 May 2018 10:51:08 -0700 Subject: [PATCH 12/43] [Beats Management] APIs: take auth tokens via headers (#19210) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Fixing minor typo in TODO comment * Make "Enroll Beat" API take enrollment token via header instead of request body * Make "Update Beat" API take access token via header instead of request body --- .../routes/api/register_enroll_beat_route.js | 8 +++++--- .../routes/api/register_update_beat_route.js | 8 +++++--- .../routes/api/register_verify_beats_route.js | 16 ++++++++-------- .../api_integration/apis/beats/enroll_beat.js | 13 +++++++------ .../api_integration/apis/beats/update_beat.js | 7 ++++--- 5 files changed, 29 insertions(+), 23 deletions(-) diff --git a/x-pack/plugins/beats/server/routes/api/register_enroll_beat_route.js b/x-pack/plugins/beats/server/routes/api/register_enroll_beat_route.js index 2a86f33b0d28f..07e336a1e091b 100644 --- a/x-pack/plugins/beats/server/routes/api/register_enroll_beat_route.js +++ b/x-pack/plugins/beats/server/routes/api/register_enroll_beat_route.js @@ -62,11 +62,13 @@ export function registerEnrollBeatRoute(server) { config: { validate: { payload: Joi.object({ - enrollment_token: Joi.string().required(), type: Joi.string().required(), version: Joi.string().required(), host_name: Joi.string().required() - }).required() + }).required(), + headers: Joi.object({ + 'kbn-beats-enrollment-token': Joi.string().required() + }).options({ allowUnknown: true }) }, auth: false }, @@ -76,7 +78,7 @@ export function registerEnrollBeatRoute(server) { let accessToken; try { - const enrollmentToken = request.payload.enrollment_token; + const enrollmentToken = request.headers['kbn-beats-enrollment-token']; const { token, expires_on: expiresOn } = await getEnrollmentToken(callWithInternalUser, enrollmentToken); if (!token || token !== enrollmentToken) { return reply({ message: 'Invalid enrollment token' }).code(400); diff --git a/x-pack/plugins/beats/server/routes/api/register_update_beat_route.js b/x-pack/plugins/beats/server/routes/api/register_update_beat_route.js index c93eca7590454..fe615ffe1a11c 100644 --- a/x-pack/plugins/beats/server/routes/api/register_update_beat_route.js +++ b/x-pack/plugins/beats/server/routes/api/register_update_beat_route.js @@ -51,14 +51,16 @@ export function registerUpdateBeatRoute(server) { config: { validate: { payload: Joi.object({ - access_token: Joi.string().required(), type: Joi.string(), version: Joi.string(), host_name: Joi.string(), ephemeral_id: Joi.string(), local_configuration_yml: Joi.string(), metadata: Joi.object() - }).required() + }).required(), + headers: Joi.object({ + 'kbn-beats-access-token': Joi.string().required() + }).options({ allowUnknown: true }) }, auth: false }, @@ -72,7 +74,7 @@ export function registerUpdateBeatRoute(server) { return reply({ message: 'Beat not found' }).code(404); } - const isAccessTokenValid = beat.access_token === request.payload.access_token; + const isAccessTokenValid = beat.access_token === request.headers['kbn-beats-access-token']; if (!isAccessTokenValid) { return reply({ message: 'Invalid access token' }).code(401); } diff --git a/x-pack/plugins/beats/server/routes/api/register_verify_beats_route.js b/x-pack/plugins/beats/server/routes/api/register_verify_beats_route.js index b2113029224a5..6aaa61b07c5f8 100644 --- a/x-pack/plugins/beats/server/routes/api/register_verify_beats_route.js +++ b/x-pack/plugins/beats/server/routes/api/register_verify_beats_route.js @@ -49,7 +49,7 @@ async function verifyBeats(callWithRequest, beatIds) { return get(response, 'items', []); } -function findNonExistentBeatIds(beatsFromEs, beatIdsFromRequest) { +function determineNonExistentBeatIds(beatsFromEs, beatIdsFromRequest) { return beatsFromEs.reduce((nonExistentBeatIds, beatFromEs, idx) => { if (!beatFromEs.found) { nonExistentBeatIds.push(beatIdsFromRequest[idx]); @@ -58,21 +58,21 @@ function findNonExistentBeatIds(beatsFromEs, beatIdsFromRequest) { }, []); } -function findAlreadyVerifiedBeatIds(beatsFromEs) { +function determineAlreadyVerifiedBeatIds(beatsFromEs) { return beatsFromEs .filter(beat => beat.found) .filter(beat => beat._source.beat.hasOwnProperty('verified_on')) .map(beat => beat._source.beat.id); } -function findToBeVerifiedBeatIds(beatsFromEs) { +function determineToBeVerifiedBeatIds(beatsFromEs) { return beatsFromEs .filter(beat => beat.found) .filter(beat => !beat._source.beat.hasOwnProperty('verified_on')) .map(beat => beat._source.beat.id); } -function findVerifiedBeatIds(verifications, toBeVerifiedBeatIds) { +function determineVerifiedBeatIds(verifications, toBeVerifiedBeatIds) { return verifications.reduce((verifiedBeatIds, verification, idx) => { if (verification.update.status === 200) { verifiedBeatIds.push(toBeVerifiedBeatIds[idx]); @@ -109,12 +109,12 @@ export function registerVerifyBeatsRoute(server) { try { const beatsFromEs = await getBeats(callWithRequest, beatIds); - nonExistentBeatIds = findNonExistentBeatIds(beatsFromEs, beatIds); - alreadyVerifiedBeatIds = findAlreadyVerifiedBeatIds(beatsFromEs); - const toBeVerifiedBeatIds = findToBeVerifiedBeatIds(beatsFromEs); + nonExistentBeatIds = determineNonExistentBeatIds(beatsFromEs, beatIds); + alreadyVerifiedBeatIds = determineAlreadyVerifiedBeatIds(beatsFromEs); + const toBeVerifiedBeatIds = determineToBeVerifiedBeatIds(beatsFromEs); const verifications = await verifyBeats(callWithRequest, toBeVerifiedBeatIds); - verifiedBeatIds = findVerifiedBeatIds(verifications, toBeVerifiedBeatIds); + verifiedBeatIds = determineVerifiedBeatIds(verifications, toBeVerifiedBeatIds); } catch (err) { return reply(wrapEsError(err)); diff --git a/x-pack/test/api_integration/apis/beats/enroll_beat.js b/x-pack/test/api_integration/apis/beats/enroll_beat.js index 388987f6d6d22..91317bca976ee 100644 --- a/x-pack/test/api_integration/apis/beats/enroll_beat.js +++ b/x-pack/test/api_integration/apis/beats/enroll_beat.js @@ -31,7 +31,6 @@ export default function ({ getService }) { + chance.integer({ min: 1, max: 10 }); beat = { - enrollment_token: validEnrollmentToken, type: 'filebeat', host_name: 'foo.bar.com', version @@ -57,6 +56,7 @@ export default function ({ getService }) { `/api/beats/agent/${beatId}` ) .set('kbn-xsrf', 'xxx') + .set('kbn-beats-enrollment-token', validEnrollmentToken) .send(beat) .expect(201); @@ -76,6 +76,7 @@ export default function ({ getService }) { `/api/beats/agent/${beatId}` ) .set('kbn-xsrf', 'xxx') + .set('kbn-beats-enrollment-token', validEnrollmentToken) .send(beat) .expect(201); @@ -94,14 +95,12 @@ export default function ({ getService }) { }); 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') + .set('kbn-beats-enrollment-token', chance.word()) .send(beat) .expect(400); @@ -124,13 +123,12 @@ export default function ({ getService }) { } }); - beat.enrollment_token = expiredEnrollmentToken; - const { body: apiResponse } = await supertest .post( `/api/beats/agent/${beatId}` ) .set('kbn-xsrf', 'xxx') + .set('kbn-beats-enrollment-token', expiredEnrollmentToken) .send(beat) .expect(400); @@ -143,6 +141,7 @@ export default function ({ getService }) { `/api/beats/agent/${beatId}` ) .set('kbn-xsrf', 'xxx') + .set('kbn-beats-enrollment-token', validEnrollmentToken) .send(beat) .expect(201); @@ -162,6 +161,7 @@ export default function ({ getService }) { `/api/beats/agent/${beatId}` ) .set('kbn-xsrf', 'xxx') + .set('kbn-beats-enrollment-token', validEnrollmentToken) .send(beat) .expect(201); @@ -183,6 +183,7 @@ export default function ({ getService }) { `/api/beats/agent/${beatId}` ) .set('kbn-xsrf', 'xxx') + .set('kbn-beats-enrollment-token', validEnrollmentToken) .send(beat) .expect(409); }); diff --git a/x-pack/test/api_integration/apis/beats/update_beat.js b/x-pack/test/api_integration/apis/beats/update_beat.js index 09d286be0fcf8..92e5771e0ef4b 100644 --- a/x-pack/test/api_integration/apis/beats/update_beat.js +++ b/x-pack/test/api_integration/apis/beats/update_beat.js @@ -29,7 +29,6 @@ export default function ({ getService }) { + chance.integer({ min: 1, max: 10 }); beat = { - access_token: '93c4a4dd08564c189a7ec4e4f046b975', type: `${chance.word()}beat`, host_name: `www.${chance.word()}.net`, version, @@ -46,6 +45,7 @@ export default function ({ getService }) { `/api/beats/agent/${beatId}` ) .set('kbn-xsrf', 'xxx') + .set('kbn-beats-access-token', '93c4a4dd08564c189a7ec4e4f046b975') .send(beat) .expect(204); @@ -64,12 +64,12 @@ export default function ({ getService }) { it('should return an error for an invalid access token', async () => { const beatId = 'foo'; - beat.access_token = chance.word(); const { body } = await supertest .put( `/api/beats/agent/${beatId}` ) .set('kbn-xsrf', 'xxx') + .set('kbn-beats-access-token', chance.word()) .send(beat) .expect(401); @@ -90,12 +90,12 @@ export default function ({ getService }) { it('should return an error for an existing but unverified beat', async () => { const beatId = 'bar'; - beat.access_token = '3c4a4dd08564c189a7ec4e4f046b9759'; const { body } = await supertest .put( `/api/beats/agent/${beatId}` ) .set('kbn-xsrf', 'xxx') + .set('kbn-beats-access-token', '3c4a4dd08564c189a7ec4e4f046b9759') .send(beat) .expect(400); @@ -121,6 +121,7 @@ export default function ({ getService }) { `/api/beats/agent/${beatId}` ) .set('kbn-xsrf', 'xxx') + .set('kbn-beats-access-token', chance.word()) .send(beat) .expect(404); From 02ee43c3c2fc2f163d9c0896aacf4470f50f35f6 Mon Sep 17 00:00:00 2001 From: Shaunak Kashyap Date: Tue, 22 May 2018 12:49:11 -0700 Subject: [PATCH 13/43] [Beats Management] APIs: Create configuration block (#19270) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Fixing minor typo in TODO comment * Implementing POST /api/beats/configuration_blocks API * Removing unnecessary escaping * Fleshing out types + adding validation for them * Making output singular (was outputs) * Removing metricbeat.inputs --- .../common/constants/configuration_blocks.js | 19 +++ .../plugins/beats/common/constants/index.js | 1 + .../plugins/beats/server/routes/api/index.js | 2 + ...gister_create_configuration_block_route.js | 105 ++++++++++++ .../apis/beats/create_configuration_block.js | 149 ++++++++++++++++++ .../test/api_integration/apis/beats/index.js | 1 + 6 files changed, 277 insertions(+) create mode 100644 x-pack/plugins/beats/common/constants/configuration_blocks.js create mode 100644 x-pack/plugins/beats/server/routes/api/register_create_configuration_block_route.js create mode 100644 x-pack/test/api_integration/apis/beats/create_configuration_block.js 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')); }); } From c5cbf1154064e298f88b12aabfb002c60e07f18a Mon Sep 17 00:00:00 2001 From: Shaunak Kashyap Date: Wed, 23 May 2018 10:47:35 -0700 Subject: [PATCH 14/43] Revert implementation of `POST /api/beats/configuration_blocks` API (#19340) This API allowed the user to operate at a level of abstraction that is unnecessarily and dangerously too low. A better API would be at one level higher, where users can create, update, and delete tags (where a tag can contain multiple configuration blocks). --- .../plugins/beats/server/routes/api/index.js | 2 - ...gister_create_configuration_block_route.js | 105 ------------ .../apis/beats/create_configuration_block.js | 149 ------------------ .../test/api_integration/apis/beats/index.js | 1 - 4 files changed, 257 deletions(-) delete mode 100644 x-pack/plugins/beats/server/routes/api/register_create_configuration_block_route.js delete mode 100644 x-pack/test/api_integration/apis/beats/create_configuration_block.js diff --git a/x-pack/plugins/beats/server/routes/api/index.js b/x-pack/plugins/beats/server/routes/api/index.js index 3b0c96834aa3a..4e6ee318668bf 100644 --- a/x-pack/plugins/beats/server/routes/api/index.js +++ b/x-pack/plugins/beats/server/routes/api/index.js @@ -9,7 +9,6 @@ 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); @@ -17,5 +16,4 @@ 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 deleted file mode 100644 index d259b705cf747..0000000000000 --- a/x-pack/plugins/beats/server/routes/api/register_create_configuration_block_route.js +++ /dev/null @@ -1,105 +0,0 @@ -/* - * 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 deleted file mode 100644 index 680ff8b2a6d21..0000000000000 --- a/x-pack/test/api_integration/apis/beats/create_configuration_block.js +++ /dev/null @@ -1,149 +0,0 @@ -/* - * 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 da94353aafeee..b41f17ed749b3 100644 --- a/x-pack/test/api_integration/apis/beats/index.js +++ b/x-pack/test/api_integration/apis/beats/index.js @@ -22,6 +22,5 @@ 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')); }); } From fbfbeba1f64b008f073e6952f9113cd60475b222 Mon Sep 17 00:00:00 2001 From: Shaunak Kashyap Date: Wed, 23 May 2018 11:22:40 -0700 Subject: [PATCH 15/43] [Beats Management] APIs: Create or update tag (#19342) * Updating mappings * Implementing PUT /api/beats/tag/{tag} API --- .../lib/index_template/beats_template.json | 22 +- .../plugins/beats/server/routes/api/index.js | 2 + .../routes/api/register_set_tag_route.js | 124 +++++++++++ .../test/api_integration/apis/beats/index.js | 1 + .../api_integration/apis/beats/set_tag.js | 207 ++++++++++++++++++ .../es_archives/beats/list/mappings.json | 22 +- 6 files changed, 364 insertions(+), 14 deletions(-) create mode 100644 x-pack/plugins/beats/server/routes/api/register_set_tag_route.js create mode 100644 x-pack/test/api_integration/apis/beats/set_tag.js diff --git a/x-pack/plugins/beats/server/lib/index_template/beats_template.json b/x-pack/plugins/beats/server/lib/index_template/beats_template.json index e293845f9a3a8..9f912f19b2a8d 100644 --- a/x-pack/plugins/beats/server/lib/index_template/beats_template.json +++ b/x-pack/plugins/beats/server/lib/index_template/beats_template.json @@ -27,16 +27,21 @@ } } }, - "configuration_block": { + "tag": { "properties": { - "tag": { - "type": "keyword" - }, - "type": { + "id": { "type": "keyword" }, - "block_yml": { - "type": "text" + "configuration_blocks": { + "type": "nested", + "properties": { + "type": { + "type": "keyword" + }, + "block_yml": { + "type": "text" + } + } } } }, @@ -69,6 +74,9 @@ "local_configuration_yml": { "type": "text" }, + "tags": { + "type": "keyword" + }, "central_configuration_yml": { "type": "text" }, diff --git a/x-pack/plugins/beats/server/routes/api/index.js b/x-pack/plugins/beats/server/routes/api/index.js index 4e6ee318668bf..5d7570807d682 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 { registerSetTagRoute } from './register_set_tag_route'; export function registerApiRoutes(server) { registerCreateEnrollmentTokensRoute(server); @@ -16,4 +17,5 @@ export function registerApiRoutes(server) { registerListBeatsRoute(server); registerVerifyBeatsRoute(server); registerUpdateBeatRoute(server); + registerSetTagRoute(server); } diff --git a/x-pack/plugins/beats/server/routes/api/register_set_tag_route.js b/x-pack/plugins/beats/server/routes/api/register_set_tag_route.js new file mode 100644 index 0000000000000..288fcade9929b --- /dev/null +++ b/x-pack/plugins/beats/server/routes/api/register_set_tag_route.js @@ -0,0 +1,124 @@ +/* + * 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 { + get, + uniq, + intersection +} from 'lodash'; +import { + INDEX_NAMES, + CONFIGURATION_BLOCKS +} from '../../../common/constants'; +import { callWithRequestFactory } from '../../lib/client'; +import { wrapEsError } from '../../lib/error_wrappers'; + +function validateUniquenessEnforcingTypes(configurationBlocks) { + const types = uniq(configurationBlocks.map(block => block.type)); + + // If none of the types in the given configuration blocks are uniqueness-enforcing, + // we don't need to perform any further validation checks. + const uniquenessEnforcingTypes = intersection(types, CONFIGURATION_BLOCKS.UNIQUENESS_ENFORCING_TYPES); + if (uniquenessEnforcingTypes.length === 0) { + return { isValid: true }; + } + + // Count the number of uniqueness-enforcing types in the given configuration blocks + const typeCountMap = configurationBlocks.reduce((typeCountMap, block) => { + const { type } = block; + if (!uniquenessEnforcingTypes.includes(type)) { + return typeCountMap; + } + + const count = typeCountMap[type] || 0; + return { + ...typeCountMap, + [type]: count + 1 + }; + }, {}); + + // If there is no more than one of any uniqueness-enforcing types in the given + // configuration blocks, we don't need to perform any further validation checks. + if (Object.values(typeCountMap).filter(count => count > 1).length === 0) { + return { isValid: true }; + } + + const message = Object.entries(typeCountMap) + .filter(([, count]) => count > 1) + .map(([type, count]) => `Expected only one configuration block of type '${type}' but found ${count}`) + .join(' '); + + return { + isValid: false, + message + }; +} + +async function validateConfigurationBlocks(configurationBlocks) { + return validateUniquenessEnforcingTypes(configurationBlocks); +} + +async function persistTag(callWithRequest, tag) { + const body = { + type: 'tag', + tag + }; + + const params = { + index: INDEX_NAMES.BEATS, + type: '_doc', + id: `tag:${tag.id}`, + body, + refresh: 'wait_for' + }; + + const response = await callWithRequest('index', params); + return response.result; +} + +// TODO: add license check pre-hook +// TODO: write to Kibana audit log file +export function registerSetTagRoute(server) { + server.route({ + method: 'PUT', + path: '/api/beats/tag/{tag}', + config: { + validate: { + payload: Joi.object({ + configuration_blocks: Joi.array().items( + Joi.object({ + type: Joi.string().required().valid(Object.values(CONFIGURATION_BLOCKS.TYPES)), + block_yml: Joi.string().required() + }) + ) + }).allow(null) + } + }, + handler: async (request, reply) => { + const callWithRequest = callWithRequestFactory(server, request); + + let result; + try { + const configurationBlocks = get(request, 'payload.configuration_blocks', []); + const { isValid, message } = await validateConfigurationBlocks(configurationBlocks); + if (!isValid) { + return reply({ message }).code(400); + } + + const tag = { + id: request.params.tag, + configuration_blocks: configurationBlocks + }; + result = await persistTag(callWithRequest, tag); + } catch (err) { + return reply(wrapEsError(err)); + } + + reply().code(result === 'created' ? 201 : 200); + } + }); +} diff --git a/x-pack/test/api_integration/apis/beats/index.js b/x-pack/test/api_integration/apis/beats/index.js index b41f17ed749b3..c3f07ecaa2926 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('./set_tag')); }); } diff --git a/x-pack/test/api_integration/apis/beats/set_tag.js b/x-pack/test/api_integration/apis/beats/set_tag.js new file mode 100644 index 0000000000000..3af3c0372e847 --- /dev/null +++ b/x-pack/test/api_integration/apis/beats/set_tag.js @@ -0,0 +1,207 @@ +/* + * 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 chance = getService('chance'); + const es = getService('es'); + + describe('set_tag', () => { + it('should create an empty tag', async () => { + const tagId = 'production'; + await supertest + .put( + `/api/beats/tag/${tagId}` + ) + .set('kbn-xsrf', 'xxx') + .send() + .expect(201); + + const esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `tag:${tagId}` + }); + + const tagInEs = esResponse._source; + + expect(tagInEs.type).to.be('tag'); + expect(tagInEs.tag.id).to.be(tagId); + expect(tagInEs.tag.configuration_blocks).to.be.an(Array); + expect(tagInEs.tag.configuration_blocks.length).to.be(0); + }); + + it('should create a tag with one configuration block', async () => { + const tagId = 'production'; + await supertest + .put( + `/api/beats/tag/${tagId}` + ) + .set('kbn-xsrf', 'xxx') + .send({ + configuration_blocks: [ + { + type: 'output', + block_yml: 'elasticsearch:\n hosts: [\"localhost:9200\"]\n username: "..."' + } + ] + }) + .expect(201); + + const esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `tag:${tagId}` + }); + + const tagInEs = esResponse._source; + + expect(tagInEs.type).to.be('tag'); + expect(tagInEs.tag.id).to.be(tagId); + expect(tagInEs.tag.configuration_blocks).to.be.an(Array); + expect(tagInEs.tag.configuration_blocks.length).to.be(1); + expect(tagInEs.tag.configuration_blocks[0].type).to.be('output'); + expect(tagInEs.tag.configuration_blocks[0].block_yml).to.be('elasticsearch:\n hosts: [\"localhost:9200\"]\n username: "..."'); + }); + + it('should create a tag with two configuration blocks', async () => { + const tagId = 'production'; + await supertest + .put( + `/api/beats/tag/${tagId}` + ) + .set('kbn-xsrf', 'xxx') + .send({ + configuration_blocks: [ + { + type: 'filebeat.inputs', + block_yml: 'file:\n path: "/var/log/some.log"]\n' + }, + { + type: 'output', + block_yml: 'elasticsearch:\n hosts: [\"localhost:9200\"]\n username: "..."' + } + ] + }) + .expect(201); + + const esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `tag:${tagId}` + }); + + const tagInEs = esResponse._source; + + expect(tagInEs.type).to.be('tag'); + expect(tagInEs.tag.id).to.be(tagId); + expect(tagInEs.tag.configuration_blocks).to.be.an(Array); + expect(tagInEs.tag.configuration_blocks.length).to.be(2); + expect(tagInEs.tag.configuration_blocks[0].type).to.be('filebeat.inputs'); + expect(tagInEs.tag.configuration_blocks[0].block_yml).to.be('file:\n path: "/var/log/some.log"]\n'); + expect(tagInEs.tag.configuration_blocks[1].type).to.be('output'); + expect(tagInEs.tag.configuration_blocks[1].block_yml).to.be('elasticsearch:\n hosts: [\"localhost:9200\"]\n username: "..."'); + }); + + it('should fail when creating a tag with two configuration blocks of type output', async () => { + const tagId = 'production'; + await supertest + .put( + `/api/beats/tag/${tagId}` + ) + .set('kbn-xsrf', 'xxx') + .send({ + configuration_blocks: [ + { + type: 'output', + block_yml: 'logstash:\n hosts: ["localhost:9000"]\n' + }, + { + type: 'output', + block_yml: 'elasticsearch:\n hosts: [\"localhost:9200\"]\n username: "..."' + } + ] + }) + .expect(400); + }); + + it('should fail when creating a tag with an invalid configuration block type', async () => { + const tagId = 'production'; + await supertest + .put( + `/api/beats/tag/${tagId}` + ) + .set('kbn-xsrf', 'xxx') + .send({ + configuration_blocks: [ + { + type: chance.word(), + block_yml: 'logstash:\n hosts: ["localhost:9000"]\n' + } + ] + }) + .expect(400); + }); + + it('should update an existing tag', async () => { + const tagId = 'production'; + await supertest + .put( + `/api/beats/tag/${tagId}` + ) + .set('kbn-xsrf', 'xxx') + .send({ + configuration_blocks: [ + { + type: 'filebeat.inputs', + block_yml: 'file:\n path: "/var/log/some.log"]\n' + }, + { + type: 'output', + block_yml: 'elasticsearch:\n hosts: [\"localhost:9200\"]\n username: "..."' + } + ] + }) + .expect(201); + + await supertest + .put( + `/api/beats/tag/${tagId}` + ) + .set('kbn-xsrf', 'xxx') + .send({ + configuration_blocks: [ + { + type: 'output', + block_yml: 'logstash:\n hosts: ["localhost:9000"]\n' + } + ] + }) + .expect(200); + + const esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `tag:${tagId}` + }); + + const tagInEs = esResponse._source; + + expect(tagInEs.type).to.be('tag'); + expect(tagInEs.tag.id).to.be(tagId); + expect(tagInEs.tag.configuration_blocks).to.be.an(Array); + expect(tagInEs.tag.configuration_blocks.length).to.be(1); + expect(tagInEs.tag.configuration_blocks[0].type).to.be('output'); + expect(tagInEs.tag.configuration_blocks[0].block_yml).to.be('logstash:\n hosts: ["localhost:9000"]\n'); + }); + }); +} diff --git a/x-pack/test/functional/es_archives/beats/list/mappings.json b/x-pack/test/functional/es_archives/beats/list/mappings.json index 24aceed8a5b61..0057b0b765773 100644 --- a/x-pack/test/functional/es_archives/beats/list/mappings.json +++ b/x-pack/test/functional/es_archives/beats/list/mappings.json @@ -27,16 +27,21 @@ } } }, - "configuration_block": { + "tag": { "properties": { - "tag": { - "type": "keyword" - }, - "type": { + "id": { "type": "keyword" }, - "block_yml": { - "type": "text" + "configuration_blocks": { + "type": "nested", + "properties": { + "type": { + "type": "keyword" + }, + "block_yml": { + "type": "text" + } + } } } }, @@ -69,6 +74,9 @@ "local_configuration_yml": { "type": "text" }, + "tags": { + "type": "keyword" + }, "central_configuration_yml": { "type": "text" }, From 1d766e37168a08b46e8c1499244b6a6c8a0242a6 Mon Sep 17 00:00:00 2001 From: Shaunak Kashyap Date: Fri, 25 May 2018 05:44:21 -0700 Subject: [PATCH 16/43] [Beats Management] Prevent timing attacks when checking auth tokens (#19363) * Using crypto.timingSafeEqual() for comparing auth tokens * Prevent subtler timing attack in token comparison function * Introduce random delay after we try to find token in ES to mitigate timing attack * Remove random delay --- .../server/lib/crypto/are_tokens_equal.js | 21 +++++++++++++++++++ .../plugins/beats/server/lib/crypto/index.js | 7 +++++++ .../routes/api/register_enroll_beat_route.js | 2 +- .../routes/api/register_update_beat_route.js | 3 ++- 4 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/beats/server/lib/crypto/are_tokens_equal.js create mode 100644 x-pack/plugins/beats/server/lib/crypto/index.js diff --git a/x-pack/plugins/beats/server/lib/crypto/are_tokens_equal.js b/x-pack/plugins/beats/server/lib/crypto/are_tokens_equal.js new file mode 100644 index 0000000000000..a6ed171d30e5e --- /dev/null +++ b/x-pack/plugins/beats/server/lib/crypto/are_tokens_equal.js @@ -0,0 +1,21 @@ +/* + * 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 { timingSafeEqual } from 'crypto'; + +const RANDOM_TOKEN_1 = 'b48c4bda384a40cb91c6eb9b8849e77f'; +const RANDOM_TOKEN_2 = '80a3819e3cd64f4399f1d4886be7a08b'; + +export function areTokensEqual(token1, token2) { + if ((typeof token1 !== 'string') || (typeof token2 !== 'string') || (token1.length !== token2.length)) { + // This prevents a more subtle timing attack where we know already the tokens aren't going to + // match but still we don't return fast. Instead we compare two pre-generated random tokens using + // the same comparison algorithm that we would use to compare two equal-length tokens. + return timingSafeEqual(Buffer.from(RANDOM_TOKEN_1, 'utf8'), Buffer.from(RANDOM_TOKEN_2, 'utf8')); + } + + return timingSafeEqual(Buffer.from(token1, 'utf8'), Buffer.from(token2, 'utf8')); +} diff --git a/x-pack/plugins/beats/server/lib/crypto/index.js b/x-pack/plugins/beats/server/lib/crypto/index.js new file mode 100644 index 0000000000000..31fa5de67b2ca --- /dev/null +++ b/x-pack/plugins/beats/server/lib/crypto/index.js @@ -0,0 +1,7 @@ +/* + * 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 { areTokensEqual } from './are_tokens_equal'; diff --git a/x-pack/plugins/beats/server/routes/api/register_enroll_beat_route.js b/x-pack/plugins/beats/server/routes/api/register_enroll_beat_route.js index 07e336a1e091b..77742c16cd401 100644 --- a/x-pack/plugins/beats/server/routes/api/register_enroll_beat_route.js +++ b/x-pack/plugins/beats/server/routes/api/register_enroll_beat_route.js @@ -80,7 +80,7 @@ export function registerEnrollBeatRoute(server) { try { const enrollmentToken = request.headers['kbn-beats-enrollment-token']; const { token, expires_on: expiresOn } = await getEnrollmentToken(callWithInternalUser, enrollmentToken); - if (!token || token !== enrollmentToken) { + if (!token) { return reply({ message: 'Invalid enrollment token' }).code(400); } if (moment(expiresOn).isBefore(moment())) { diff --git a/x-pack/plugins/beats/server/routes/api/register_update_beat_route.js b/x-pack/plugins/beats/server/routes/api/register_update_beat_route.js index fe615ffe1a11c..5955e65f6bbaf 100644 --- a/x-pack/plugins/beats/server/routes/api/register_update_beat_route.js +++ b/x-pack/plugins/beats/server/routes/api/register_update_beat_route.js @@ -9,6 +9,7 @@ import { get } from 'lodash'; import { INDEX_NAMES } from '../../../common/constants'; import { callWithInternalUserFactory } from '../../lib/client'; import { wrapEsError } from '../../lib/error_wrappers'; +import { areTokensEqual } from '../../lib/crypto'; async function getBeat(callWithInternalUser, beatId) { const params = { @@ -74,7 +75,7 @@ export function registerUpdateBeatRoute(server) { return reply({ message: 'Beat not found' }).code(404); } - const isAccessTokenValid = beat.access_token === request.headers['kbn-beats-access-token']; + const isAccessTokenValid = areTokensEqual(beat.access_token, request.headers['kbn-beats-access-token']); if (!isAccessTokenValid) { return reply({ message: 'Invalid access token' }).code(401); } From d2ec2b7f281f61ac5bf6cf2c99f01c249b15a67a Mon Sep 17 00:00:00 2001 From: Shaunak Kashyap Date: Thu, 31 May 2018 15:54:12 -0700 Subject: [PATCH 17/43] [Beats Management] APIs: Assign tag(s) to beat(s) (#19431) * Using crypto.timingSafeEqual() for comparing auth tokens * Introduce random delay after we try to find token in ES to mitigate timing attack * Rename "determine" to "find" * Remove random delay * Starting to implement POST /api/beats/beats_tags API * Changing API * Updating tests for changes to API * Updating ES archive * Renaming * Use destructuring * Moving start of script to own line to increase readability * Using destructuring --- .../plugins/beats/server/routes/api/index.js | 2 + .../register_assign_tags_to_beats_route.js | 169 ++++++++++ .../routes/api/register_verify_beats_route.js | 16 +- .../apis/beats/assign_tags_to_beats.js | 290 ++++++++++++++++++ .../test/api_integration/apis/beats/index.js | 1 + .../es_archives/beats/list/data.json.gz | Bin 371 -> 436 bytes 6 files changed, 470 insertions(+), 8 deletions(-) create mode 100644 x-pack/plugins/beats/server/routes/api/register_assign_tags_to_beats_route.js create mode 100644 x-pack/test/api_integration/apis/beats/assign_tags_to_beats.js diff --git a/x-pack/plugins/beats/server/routes/api/index.js b/x-pack/plugins/beats/server/routes/api/index.js index 5d7570807d682..aa1be44cd96ca 100644 --- a/x-pack/plugins/beats/server/routes/api/index.js +++ b/x-pack/plugins/beats/server/routes/api/index.js @@ -10,6 +10,7 @@ import { registerListBeatsRoute } from './register_list_beats_route'; import { registerVerifyBeatsRoute } from './register_verify_beats_route'; import { registerUpdateBeatRoute } from './register_update_beat_route'; import { registerSetTagRoute } from './register_set_tag_route'; +import { registerAssignTagsToBeatsRoute } from './register_assign_tags_to_beats_route'; export function registerApiRoutes(server) { registerCreateEnrollmentTokensRoute(server); @@ -18,4 +19,5 @@ export function registerApiRoutes(server) { registerVerifyBeatsRoute(server); registerUpdateBeatRoute(server); registerSetTagRoute(server); + registerAssignTagsToBeatsRoute(server); } diff --git a/x-pack/plugins/beats/server/routes/api/register_assign_tags_to_beats_route.js b/x-pack/plugins/beats/server/routes/api/register_assign_tags_to_beats_route.js new file mode 100644 index 0000000000000..5f6c5f7a7b906 --- /dev/null +++ b/x-pack/plugins/beats/server/routes/api/register_assign_tags_to_beats_route.js @@ -0,0 +1,169 @@ +/* + * 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 { + get, + flatten, + uniq +} from 'lodash'; +import { INDEX_NAMES } from '../../../common/constants'; +import { callWithRequestFactory } from '../../lib/client'; +import { wrapEsError } from '../../lib/error_wrappers'; + +async function getDocs(callWithRequest, ids) { + const params = { + index: INDEX_NAMES.BEATS, + type: '_doc', + body: { ids }, + _source: false + }; + + const response = await callWithRequest('mget', params); + return get(response, 'docs', []); +} + +function getBeats(callWithRequest, beatIds) { + const ids = beatIds.map(beatId => `beat:${beatId}`); + return getDocs(callWithRequest, ids); +} + +function getTags(callWithRequest, tags) { + const ids = tags.map(tag => `tag:${tag}`); + return getDocs(callWithRequest, ids); +} + +async function findNonExistentItems(callWithRequest, items, getFn) { + const itemsFromEs = await getFn.call(null, callWithRequest, items); + return itemsFromEs.reduce((nonExistentItems, itemFromEs, idx) => { + if (!itemFromEs.found) { + nonExistentItems.push(items[idx]); + } + return nonExistentItems; + }, []); +} + +function findNonExistentBeatIds(callWithRequest, beatIds) { + return findNonExistentItems(callWithRequest, beatIds, getBeats); +} + +function findNonExistentTags(callWithRequest, tags) { + return findNonExistentItems(callWithRequest, tags, getTags); +} + +async function persistAssignments(callWithRequest, assignments) { + const body = flatten(assignments.map(({ beatId, tag }) => { + const script = '' + + 'def beat = ctx._source.beat; ' + + 'if (beat.tags == null) { ' + + ' beat.tags = []; ' + + '} ' + + 'if (!beat.tags.contains(params.tag)) { ' + + ' beat.tags.add(params.tag); ' + + '}'; + + return [ + { update: { _id: `beat:${beatId}` } }, + { script: { source: script, params: { tag } } } + ]; + })); + + const params = { + index: INDEX_NAMES.BEATS, + type: '_doc', + body, + refresh: 'wait_for' + }; + + const response = await callWithRequest('bulk', params); + return get(response, 'items', []) + .map((item, resultIdx) => ({ + status: item.update.status, + result: item.update.result, + idxInRequest: assignments[resultIdx].idxInRequest + })); +} + +function addNonExistentItemAssignmentsToResponse(response, assignments, nonExistentBeatIds, nonExistentTags) { + assignments.forEach(({ beat_id: beatId, tag }, idx) => { + const isBeatNonExistent = nonExistentBeatIds.includes(beatId); + const isTagNonExistent = nonExistentTags.includes(tag); + + if (isBeatNonExistent && isTagNonExistent) { + response.assignments[idx].status = 404; + response.assignments[idx].result = `Beat ${beatId} and tag ${tag} not found`; + } else if (isBeatNonExistent) { + response.assignments[idx].status = 404; + response.assignments[idx].result = `Beat ${beatId} not found`; + } else if (isTagNonExistent) { + response.assignments[idx].status = 404; + response.assignments[idx].result = `Tag ${tag} not found`; + } + }); +} + +function addAssignmentResultsToResponse(response, assignmentResults) { + assignmentResults.forEach(assignmentResult => { + const { idxInRequest, status, result } = assignmentResult; + response.assignments[idxInRequest].status = status; + response.assignments[idxInRequest].result = result; + }); +} + +// TODO: add license check pre-hook +// TODO: write to Kibana audit log file +export function registerAssignTagsToBeatsRoute(server) { + server.route({ + method: 'POST', + path: '/api/beats/agents_tags/assignments', + config: { + validate: { + payload: Joi.object({ + assignments: Joi.array().items(Joi.object({ + beat_id: Joi.string().required(), + tag: Joi.string().required() + })) + }).required() + } + }, + handler: async (request, reply) => { + const callWithRequest = callWithRequestFactory(server, request); + + const { assignments } = request.payload; + const beatIds = uniq(assignments.map(assignment => assignment.beat_id)); + const tags = uniq(assignments.map(assignment => assignment.tag)); + + const response = { + assignments: assignments.map(() => ({ status: null })) + }; + + try { + // Handle assignments containing non-existing beat IDs or tags + const nonExistentBeatIds = await findNonExistentBeatIds(callWithRequest, beatIds); + const nonExistentTags = await findNonExistentTags(callWithRequest, tags); + + addNonExistentItemAssignmentsToResponse(response, assignments, nonExistentBeatIds, nonExistentTags); + + const validAssignments = assignments + .map((assignment, idxInRequest) => ({ + beatId: assignment.beat_id, + tag: assignment.tag, + idxInRequest // so we can add the result of this assignment to the correct place in the response + })) + .filter((assignment, idx) => response.assignments[idx].status === null); + + if (validAssignments.length > 0) { + const assignmentResults = await persistAssignments(callWithRequest, validAssignments); + addAssignmentResultsToResponse(response, assignmentResults); + } + } catch (err) { + return reply(wrapEsError(err)); + } + + reply(response); + } + }); +} diff --git a/x-pack/plugins/beats/server/routes/api/register_verify_beats_route.js b/x-pack/plugins/beats/server/routes/api/register_verify_beats_route.js index 6aaa61b07c5f8..b2113029224a5 100644 --- a/x-pack/plugins/beats/server/routes/api/register_verify_beats_route.js +++ b/x-pack/plugins/beats/server/routes/api/register_verify_beats_route.js @@ -49,7 +49,7 @@ async function verifyBeats(callWithRequest, beatIds) { return get(response, 'items', []); } -function determineNonExistentBeatIds(beatsFromEs, beatIdsFromRequest) { +function findNonExistentBeatIds(beatsFromEs, beatIdsFromRequest) { return beatsFromEs.reduce((nonExistentBeatIds, beatFromEs, idx) => { if (!beatFromEs.found) { nonExistentBeatIds.push(beatIdsFromRequest[idx]); @@ -58,21 +58,21 @@ function determineNonExistentBeatIds(beatsFromEs, beatIdsFromRequest) { }, []); } -function determineAlreadyVerifiedBeatIds(beatsFromEs) { +function findAlreadyVerifiedBeatIds(beatsFromEs) { return beatsFromEs .filter(beat => beat.found) .filter(beat => beat._source.beat.hasOwnProperty('verified_on')) .map(beat => beat._source.beat.id); } -function determineToBeVerifiedBeatIds(beatsFromEs) { +function findToBeVerifiedBeatIds(beatsFromEs) { return beatsFromEs .filter(beat => beat.found) .filter(beat => !beat._source.beat.hasOwnProperty('verified_on')) .map(beat => beat._source.beat.id); } -function determineVerifiedBeatIds(verifications, toBeVerifiedBeatIds) { +function findVerifiedBeatIds(verifications, toBeVerifiedBeatIds) { return verifications.reduce((verifiedBeatIds, verification, idx) => { if (verification.update.status === 200) { verifiedBeatIds.push(toBeVerifiedBeatIds[idx]); @@ -109,12 +109,12 @@ export function registerVerifyBeatsRoute(server) { try { const beatsFromEs = await getBeats(callWithRequest, beatIds); - nonExistentBeatIds = determineNonExistentBeatIds(beatsFromEs, beatIds); - alreadyVerifiedBeatIds = determineAlreadyVerifiedBeatIds(beatsFromEs); - const toBeVerifiedBeatIds = determineToBeVerifiedBeatIds(beatsFromEs); + nonExistentBeatIds = findNonExistentBeatIds(beatsFromEs, beatIds); + alreadyVerifiedBeatIds = findAlreadyVerifiedBeatIds(beatsFromEs); + const toBeVerifiedBeatIds = findToBeVerifiedBeatIds(beatsFromEs); const verifications = await verifyBeats(callWithRequest, toBeVerifiedBeatIds); - verifiedBeatIds = determineVerifiedBeatIds(verifications, toBeVerifiedBeatIds); + verifiedBeatIds = findVerifiedBeatIds(verifications, toBeVerifiedBeatIds); } catch (err) { return reply(wrapEsError(err)); diff --git a/x-pack/test/api_integration/apis/beats/assign_tags_to_beats.js b/x-pack/test/api_integration/apis/beats/assign_tags_to_beats.js new file mode 100644 index 0000000000000..a8f542239d22e --- /dev/null +++ b/x-pack/test/api_integration/apis/beats/assign_tags_to_beats.js @@ -0,0 +1,290 @@ +/* + * 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 esArchiver = getService('esArchiver'); + const es = getService('es'); + const chance = getService('chance'); + + describe('assign_tags_to_beats', () => { + const archive = 'beats/list'; + + beforeEach('load beats archive', () => esArchiver.load(archive)); + afterEach('unload beats archive', () => esArchiver.unload(archive)); + + it('should add a single tag to a single beat', async () => { + const { body: apiResponse } = await supertest + .post( + '/api/beats/agents_tags/assignments' + ) + .set('kbn-xsrf', 'xxx') + .send({ + assignments: [ + { beat_id: 'bar', tag: 'production' } + ] + }) + .expect(200); + + expect(apiResponse.assignments).to.eql([ + { status: 200, result: 'updated' } + ]); + + const esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:bar` + }); + + const beat = esResponse._source.beat; + expect(beat.tags).to.eql(['production']); + }); + + it('should not re-add an existing tag to a beat', async () => { + const tags = ['production']; + + let esResponse; + let beat; + + // Before adding the existing tag + esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:foo` + }); + + beat = esResponse._source.beat; + expect(beat.tags).to.eql(tags); + + // Adding the existing tag + const { body: apiResponse } = await supertest + .post( + '/api/beats/agents_tags/assignments' + ) + .set('kbn-xsrf', 'xxx') + .send({ + assignments: [ + { beat_id: 'foo', tag: 'production' } + ] + }) + .expect(200); + + expect(apiResponse.assignments).to.eql([ + { status: 200, result: 'updated' } + ]); + + // After adding the existing tag + esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:foo` + }); + + beat = esResponse._source.beat; + expect(beat.tags).to.eql(tags); + }); + + it('should add a single tag to a multiple beats', async () => { + const { body: apiResponse } = await supertest + .post( + '/api/beats/agents_tags/assignments' + ) + .set('kbn-xsrf', 'xxx') + .send({ + assignments: [ + { beat_id: 'foo', tag: 'development' }, + { beat_id: 'bar', tag: 'development' } + ] + }) + .expect(200); + + expect(apiResponse.assignments).to.eql([ + { status: 200, result: 'updated' }, + { status: 200, result: 'updated' } + ]); + + let esResponse; + let beat; + + // Beat foo + esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:foo` + }); + + beat = esResponse._source.beat; + expect(beat.tags).to.eql(['production', 'development']); // as beat 'foo' already had 'production' tag attached to it + + // Beat bar + esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:bar` + }); + + beat = esResponse._source.beat; + expect(beat.tags).to.eql(['development']); + }); + + it('should add multiple tags to a single beat', async () => { + const { body: apiResponse } = await supertest + .post( + '/api/beats/agents_tags/assignments' + ) + .set('kbn-xsrf', 'xxx') + .send({ + assignments: [ + { beat_id: 'bar', tag: 'development' }, + { beat_id: 'bar', tag: 'production' } + ] + }) + .expect(200); + + expect(apiResponse.assignments).to.eql([ + { status: 200, result: 'updated' }, + { status: 200, result: 'updated' } + ]); + + const esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:bar` + }); + + const beat = esResponse._source.beat; + expect(beat.tags).to.eql(['development', 'production']); + }); + + it('should add multiple tags to a multiple beats', async () => { + const { body: apiResponse } = await supertest + .post( + '/api/beats/agents_tags/assignments' + ) + .set('kbn-xsrf', 'xxx') + .send({ + assignments: [ + { beat_id: 'foo', tag: 'development' }, + { beat_id: 'bar', tag: 'production' } + ] + }) + .expect(200); + + expect(apiResponse.assignments).to.eql([ + { status: 200, result: 'updated' }, + { status: 200, result: 'updated' } + ]); + + let esResponse; + let beat; + + // Beat foo + esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:foo` + }); + + beat = esResponse._source.beat; + expect(beat.tags).to.eql(['production', 'development']); // as beat 'foo' already had 'production' tag attached to it + + // Beat bar + esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:bar` + }); + + beat = esResponse._source.beat; + expect(beat.tags).to.eql(['production']); + }); + + it('should return errors for non-existent beats', async () => { + const nonExistentBeatId = chance.word(); + + const { body: apiResponse } = await supertest + .post( + '/api/beats/agents_tags/assignments' + ) + .set('kbn-xsrf', 'xxx') + .send({ + assignments: [ + { beat_id: nonExistentBeatId, tag: 'production' } + ] + }) + .expect(200); + + expect(apiResponse.assignments).to.eql([ + { status: 404, result: `Beat ${nonExistentBeatId} not found` } + ]); + }); + + it('should return errors for non-existent tags', async () => { + const nonExistentTag = chance.word(); + + const { body: apiResponse } = await supertest + .post( + '/api/beats/agents_tags/assignments' + ) + .set('kbn-xsrf', 'xxx') + .send({ + assignments: [ + { beat_id: 'bar', tag: nonExistentTag } + ] + }) + .expect(200); + + expect(apiResponse.assignments).to.eql([ + { status: 404, result: `Tag ${nonExistentTag} not found` } + ]); + + const esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:bar` + }); + + const beat = esResponse._source.beat; + expect(beat).to.not.have.property('tags'); + }); + + it('should return errors for non-existent beats and tags', async () => { + const nonExistentBeatId = chance.word(); + const nonExistentTag = chance.word(); + + const { body: apiResponse } = await supertest + .post( + '/api/beats/agents_tags/assignments' + ) + .set('kbn-xsrf', 'xxx') + .send({ + assignments: [ + { beat_id: nonExistentBeatId, tag: nonExistentTag } + ] + }) + .expect(200); + + expect(apiResponse.assignments).to.eql([ + { status: 404, result: `Beat ${nonExistentBeatId} and tag ${nonExistentTag} not found` } + ]); + + const esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:bar` + }); + + const beat = esResponse._source.beat; + expect(beat).to.not.have.property('tags'); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/beats/index.js b/x-pack/test/api_integration/apis/beats/index.js index c3f07ecaa2926..76f712c05de44 100644 --- a/x-pack/test/api_integration/apis/beats/index.js +++ b/x-pack/test/api_integration/apis/beats/index.js @@ -23,5 +23,6 @@ export default function ({ getService, loadTestFile }) { loadTestFile(require.resolve('./verify_beats')); loadTestFile(require.resolve('./update_beat')); loadTestFile(require.resolve('./set_tag')); + loadTestFile(require.resolve('./assign_tags_to_beats')); }); } diff --git a/x-pack/test/functional/es_archives/beats/list/data.json.gz b/x-pack/test/functional/es_archives/beats/list/data.json.gz index f3ccd2687455692022cc5fb6622b906cd0ec6293..48094292337780ce8c10a685ddda5f9446160c5c 100644 GIT binary patch literal 436 zcmV;l0ZaZLiwFp-X$M;X17u-zVJ>QOZ*Bm^RLgGTFbur=D-53-!Ez);@^|d1MPX19 zoi!R;sUJbQLH>OuCxKmTlODQAE(S=>5Y!CmLlnnJ|FOj+j}z-m@)Qy~*bE_a@PQAj z#^OEzNDU3FvBsufXoS8S;j<3KrA1)bkO{E-eb`^Jof}#+^`3D9@{Eel(S(}e%4&n3 zu)g-&b$wB7Lz$9{ED8Ik+CY7xJ4ZCM#JGBnIZZnaIwrvw?7E_NZ`g#g0%4Q_OiL@7 zPKA_Itx`&bGFAf9$(eeLF5hExjH1I`MAq=<|A|~<-&W>^%$ZktNhl@ky3~H6>rzkS zeknDTGUFUI7b!5tq+Vr$3&T+<11QReO6_;(j#B?XbU$|vy{q3$`_RXq9V_DzLZ2|?0HV)S!C2LJ#d`O}C1 literal 371 zcmV-(0gV11iwFp7ua17u-zVJ>QOZ*Bm^RKae8Fbuu(6^Qd1C6J^E-?7s!$VtqG zHb@7w>Q?pNM`$aU)^>+Y?In`!_pGO9JG&^3lm26cNggN8+vFi6Ht@C%ncWZ!VbwU? z1^}s{foH6-=@$l}??(8nLvd;mST1A&EPr2bPub3|TRZihaRc&*ijUERn&Hao4ZmTB z+Kcb{qFRMABPq!U|50tAKG3}<23lf$J;xl>PC~~dSc_d(^!^o_P}U%=)_}BhiW@4G zVtPp#liE53+$2ZpK03YoXdgwpo0x3i^Z!h)v2QDT#pZNyIU|e_e%b0l(PgVAxo53r zN~J=e5t0JuT$yDmg|q^-wt%v{tJT8}-O%bkZS*Ad{6=S%19y%pA-QEr!xAk=UQ#ZN zUWz$)gKbq-=n6klQ_9qWiUkvoOy;S`GevaDpYD7F?d^V=VKCzrTseU-n+xmTUYpA= RW6|eL{sI(FHM`CQ003wLvN`|& From 29e8cf258ae9a2bc22867cc159a6d39c03a4a933 Mon Sep 17 00:00:00 2001 From: Shaunak Kashyap Date: Thu, 31 May 2018 16:01:27 -0700 Subject: [PATCH 18/43] [Beats Management] APIs: Remove tag(s) from beat(s) (#19440) * Using crypto.timingSafeEqual() for comparing auth tokens * Introduce random delay after we try to find token in ES to mitigate timing attack * Remove random delay * Starting to implement POST /api/beats/beats_tags API * Changing API * Updating tests for changes to API * Renaming * Use destructuring * Using crypto.timingSafeEqual() for comparing auth tokens * Introduce random delay after we try to find token in ES to mitigate timing attack * Implementing `POST /api/beats/agents_tags/removals` API * Updating ES archive * Use destructuring * Moving start of script to own line to increase readability * Nothing to remove if there are no existing tags! * Updating tests to match changes in bulk update painless script * Use destructuring --- .../plugins/beats/server/routes/api/index.js | 2 + .../routes/api/register_enroll_beat_route.js | 9 +- .../register_remove_tags_from_beats_route.js | 166 ++++++++++++ .../apis/beats/assign_tags_to_beats.js | 8 +- .../test/api_integration/apis/beats/index.js | 1 + .../apis/beats/remove_tags_from_beats.js | 246 ++++++++++++++++++ .../es_archives/beats/list/data.json.gz | Bin 436 -> 447 bytes 7 files changed, 427 insertions(+), 5 deletions(-) create mode 100644 x-pack/plugins/beats/server/routes/api/register_remove_tags_from_beats_route.js create mode 100644 x-pack/test/api_integration/apis/beats/remove_tags_from_beats.js diff --git a/x-pack/plugins/beats/server/routes/api/index.js b/x-pack/plugins/beats/server/routes/api/index.js index aa1be44cd96ca..6ec0ad737352a 100644 --- a/x-pack/plugins/beats/server/routes/api/index.js +++ b/x-pack/plugins/beats/server/routes/api/index.js @@ -11,6 +11,7 @@ import { registerVerifyBeatsRoute } from './register_verify_beats_route'; import { registerUpdateBeatRoute } from './register_update_beat_route'; import { registerSetTagRoute } from './register_set_tag_route'; import { registerAssignTagsToBeatsRoute } from './register_assign_tags_to_beats_route'; +import { registerRemoveTagsFromBeatsRoute } from './register_remove_tags_from_beats_route'; export function registerApiRoutes(server) { registerCreateEnrollmentTokensRoute(server); @@ -20,4 +21,5 @@ export function registerApiRoutes(server) { registerUpdateBeatRoute(server); registerSetTagRoute(server); registerAssignTagsToBeatsRoute(server); + registerRemoveTagsFromBeatsRoute(server); } diff --git a/x-pack/plugins/beats/server/routes/api/register_enroll_beat_route.js b/x-pack/plugins/beats/server/routes/api/register_enroll_beat_route.js index 77742c16cd401..bad28c0ab9be5 100644 --- a/x-pack/plugins/beats/server/routes/api/register_enroll_beat_route.js +++ b/x-pack/plugins/beats/server/routes/api/register_enroll_beat_route.js @@ -24,7 +24,14 @@ async function getEnrollmentToken(callWithInternalUser, enrollmentToken) { }; const response = await callWithInternalUser('get', params); - return get(response, '_source.enrollment_token', {}); + const token = get(response, '_source.enrollment_token', {}); + + // Elasticsearch might return fast if the token is not found. OR it might return fast + // if the token *is* found. Either way, an attacker could using a timing attack to figure + // out whether a token is valid or not. So we introduce a random delay in returning from + // this function to obscure the actual time it took for Elasticsearch to find the token. + const randomDelayInMs = 25 + Math.round(Math.random() * 200); // between 25 and 225 ms + return new Promise(resolve => setTimeout(() => resolve(token), randomDelayInMs)); } function deleteUsedEnrollmentToken(callWithInternalUser, enrollmentToken) { diff --git a/x-pack/plugins/beats/server/routes/api/register_remove_tags_from_beats_route.js b/x-pack/plugins/beats/server/routes/api/register_remove_tags_from_beats_route.js new file mode 100644 index 0000000000000..b5e66267b2ea4 --- /dev/null +++ b/x-pack/plugins/beats/server/routes/api/register_remove_tags_from_beats_route.js @@ -0,0 +1,166 @@ +/* + * 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 { + get, + flatten, + uniq +} from 'lodash'; +import { INDEX_NAMES } from '../../../common/constants'; +import { callWithRequestFactory } from '../../lib/client'; +import { wrapEsError } from '../../lib/error_wrappers'; + +async function getDocs(callWithRequest, ids) { + const params = { + index: INDEX_NAMES.BEATS, + type: '_doc', + body: { ids }, + _source: false + }; + + const response = await callWithRequest('mget', params); + return get(response, 'docs', []); +} + +function getBeats(callWithRequest, beatIds) { + const ids = beatIds.map(beatId => `beat:${beatId}`); + return getDocs(callWithRequest, ids); +} + +function getTags(callWithRequest, tags) { + const ids = tags.map(tag => `tag:${tag}`); + return getDocs(callWithRequest, ids); +} + +async function findNonExistentItems(callWithRequest, items, getFn) { + const itemsFromEs = await getFn.call(null, callWithRequest, items); + return itemsFromEs.reduce((nonExistentItems, itemFromEs, idx) => { + if (!itemFromEs.found) { + nonExistentItems.push(items[idx]); + } + return nonExistentItems; + }, []); +} + +function findNonExistentBeatIds(callWithRequest, beatIds) { + return findNonExistentItems(callWithRequest, beatIds, getBeats); +} + +function findNonExistentTags(callWithRequest, tags) { + return findNonExistentItems(callWithRequest, tags, getTags); +} + +async function persistRemovals(callWithRequest, removals) { + const body = flatten(removals.map(({ beatId, tag }) => { + const script = '' + + 'def beat = ctx._source.beat; ' + + 'if (beat.tags != null) { ' + + ' beat.tags.removeAll([params.tag]); ' + + '}'; + + return [ + { update: { _id: `beat:${beatId}` } }, + { script: { source: script, params: { tag } } } + ]; + })); + + const params = { + index: INDEX_NAMES.BEATS, + type: '_doc', + body, + refresh: 'wait_for' + }; + + const response = await callWithRequest('bulk', params); + return get(response, 'items', []) + .map((item, resultIdx) => ({ + status: item.update.status, + result: item.update.result, + idxInRequest: removals[resultIdx].idxInRequest + })); +} + +function addNonExistentItemRemovalsToResponse(response, removals, nonExistentBeatIds, nonExistentTags) { + removals.forEach(({ beat_id: beatId, tag }, idx) => { + const isBeatNonExistent = nonExistentBeatIds.includes(beatId); + const isTagNonExistent = nonExistentTags.includes(tag); + + if (isBeatNonExistent && isTagNonExistent) { + response.removals[idx].status = 404; + response.removals[idx].result = `Beat ${beatId} and tag ${tag} not found`; + } else if (isBeatNonExistent) { + response.removals[idx].status = 404; + response.removals[idx].result = `Beat ${beatId} not found`; + } else if (isTagNonExistent) { + response.removals[idx].status = 404; + response.removals[idx].result = `Tag ${tag} not found`; + } + }); +} + +function addRemovalResultsToResponse(response, removalResults) { + removalResults.forEach(removalResult => { + const { idxInRequest, status, result } = removalResult; + response.removals[idxInRequest].status = status; + response.removals[idxInRequest].result = result; + }); +} + +// TODO: add license check pre-hook +// TODO: write to Kibana audit log file +export function registerRemoveTagsFromBeatsRoute(server) { + server.route({ + method: 'POST', + path: '/api/beats/agents_tags/removals', + config: { + validate: { + payload: Joi.object({ + removals: Joi.array().items(Joi.object({ + beat_id: Joi.string().required(), + tag: Joi.string().required() + })) + }).required() + } + }, + handler: async (request, reply) => { + const callWithRequest = callWithRequestFactory(server, request); + + const { removals } = request.payload; + const beatIds = uniq(removals.map(removal => removal.beat_id)); + const tags = uniq(removals.map(removal => removal.tag)); + + const response = { + removals: removals.map(() => ({ status: null })) + }; + + try { + // Handle removals containing non-existing beat IDs or tags + const nonExistentBeatIds = await findNonExistentBeatIds(callWithRequest, beatIds); + const nonExistentTags = await findNonExistentTags(callWithRequest, tags); + + addNonExistentItemRemovalsToResponse(response, removals, nonExistentBeatIds, nonExistentTags); + + const validRemovals = removals + .map((removal, idxInRequest) => ({ + beatId: removal.beat_id, + tag: removal.tag, + idxInRequest // so we can add the result of this removal to the correct place in the response + })) + .filter((removal, idx) => response.removals[idx].status === null); + + if (validRemovals.length > 0) { + const removalResults = await persistRemovals(callWithRequest, validRemovals); + addRemovalResultsToResponse(response, removalResults); + } + } catch (err) { + return reply(wrapEsError(err)); + } + + reply(response); + } + }); +} diff --git a/x-pack/test/api_integration/apis/beats/assign_tags_to_beats.js b/x-pack/test/api_integration/apis/beats/assign_tags_to_beats.js index a8f542239d22e..88b7b7c3feb3a 100644 --- a/x-pack/test/api_integration/apis/beats/assign_tags_to_beats.js +++ b/x-pack/test/api_integration/apis/beats/assign_tags_to_beats.js @@ -63,7 +63,7 @@ export default function ({ getService }) { }); beat = esResponse._source.beat; - expect(beat.tags).to.eql(tags); + expect(beat.tags).to.eql([...tags, 'qa']); // Adding the existing tag const { body: apiResponse } = await supertest @@ -90,7 +90,7 @@ export default function ({ getService }) { }); beat = esResponse._source.beat; - expect(beat.tags).to.eql(tags); + expect(beat.tags).to.eql([...tags, 'qa']); }); it('should add a single tag to a multiple beats', async () => { @@ -123,7 +123,7 @@ export default function ({ getService }) { }); beat = esResponse._source.beat; - expect(beat.tags).to.eql(['production', 'development']); // as beat 'foo' already had 'production' tag attached to it + expect(beat.tags).to.eql(['production', 'qa', 'development']); // as beat 'foo' already had 'production' and 'qa' tags attached to it // Beat bar esResponse = await es.get({ @@ -195,7 +195,7 @@ export default function ({ getService }) { }); beat = esResponse._source.beat; - expect(beat.tags).to.eql(['production', 'development']); // as beat 'foo' already had 'production' tag attached to it + expect(beat.tags).to.eql(['production', 'qa', 'development']); // as beat 'foo' already had 'production' and 'qa' tags attached to it // Beat bar esResponse = await es.get({ diff --git a/x-pack/test/api_integration/apis/beats/index.js b/x-pack/test/api_integration/apis/beats/index.js index 76f712c05de44..f8956d3e498ba 100644 --- a/x-pack/test/api_integration/apis/beats/index.js +++ b/x-pack/test/api_integration/apis/beats/index.js @@ -24,5 +24,6 @@ export default function ({ getService, loadTestFile }) { loadTestFile(require.resolve('./update_beat')); loadTestFile(require.resolve('./set_tag')); loadTestFile(require.resolve('./assign_tags_to_beats')); + loadTestFile(require.resolve('./remove_tags_from_beats')); }); } diff --git a/x-pack/test/api_integration/apis/beats/remove_tags_from_beats.js b/x-pack/test/api_integration/apis/beats/remove_tags_from_beats.js new file mode 100644 index 0000000000000..19583f9279732 --- /dev/null +++ b/x-pack/test/api_integration/apis/beats/remove_tags_from_beats.js @@ -0,0 +1,246 @@ +/* + * 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 esArchiver = getService('esArchiver'); + const es = getService('es'); + const chance = getService('chance'); + + describe('remove_tags_from_beats', () => { + const archive = 'beats/list'; + + beforeEach('load beats archive', () => esArchiver.load(archive)); + afterEach('unload beats archive', () => esArchiver.unload(archive)); + + it('should remove a single tag from a single beat', async () => { + const { body: apiResponse } = await supertest + .post( + '/api/beats/agents_tags/removals' + ) + .set('kbn-xsrf', 'xxx') + .send({ + removals: [ + { beat_id: 'foo', tag: 'production' } + ] + }) + .expect(200); + + expect(apiResponse.removals).to.eql([ + { status: 200, result: 'updated' } + ]); + + const esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:foo` + }); + + const beat = esResponse._source.beat; + expect(beat.tags).to.eql(['qa']); + }); + + it('should remove a single tag from a multiple beats', async () => { + const { body: apiResponse } = await supertest + .post( + '/api/beats/agents_tags/removals' + ) + .set('kbn-xsrf', 'xxx') + .send({ + removals: [ + { beat_id: 'foo', tag: 'development' }, + { beat_id: 'bar', tag: 'development' } + ] + }) + .expect(200); + + expect(apiResponse.removals).to.eql([ + { status: 200, result: 'updated' }, + { status: 200, result: 'updated' } + ]); + + let esResponse; + let beat; + + // Beat foo + esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:foo` + }); + + beat = esResponse._source.beat; + expect(beat.tags).to.eql(['production', 'qa' ]); // as beat 'foo' already had 'production' and 'qa' tags attached to it + + // Beat bar + esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:bar` + }); + + beat = esResponse._source.beat; + expect(beat).to.not.have.property('tags'); + }); + + it('should remove multiple tags from a single beat', async () => { + const { body: apiResponse } = await supertest + .post( + '/api/beats/agents_tags/removals' + ) + .set('kbn-xsrf', 'xxx') + .send({ + removals: [ + { beat_id: 'foo', tag: 'development' }, + { beat_id: 'foo', tag: 'production' } + ] + }) + .expect(200); + + expect(apiResponse.removals).to.eql([ + { status: 200, result: 'updated' }, + { status: 200, result: 'updated' } + ]); + + const esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:foo` + }); + + const beat = esResponse._source.beat; + expect(beat.tags).to.eql(['qa']); // as beat 'foo' already had 'production' and 'qa' tags attached to it + }); + + it('should remove multiple tags from a multiple beats', async () => { + const { body: apiResponse } = await supertest + .post( + '/api/beats/agents_tags/removals' + ) + .set('kbn-xsrf', 'xxx') + .send({ + removals: [ + { beat_id: 'foo', tag: 'production' }, + { beat_id: 'bar', tag: 'development' } + ] + }) + .expect(200); + + expect(apiResponse.removals).to.eql([ + { status: 200, result: 'updated' }, + { status: 200, result: 'updated' } + ]); + + let esResponse; + let beat; + + // Beat foo + esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:foo` + }); + + beat = esResponse._source.beat; + expect(beat.tags).to.eql(['qa']); // as beat 'foo' already had 'production' and 'qa' tags attached to it + + // Beat bar + esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:bar` + }); + + beat = esResponse._source.beat; + expect(beat).to.not.have.property('tags'); + }); + + it('should return errors for non-existent beats', async () => { + const nonExistentBeatId = chance.word(); + + const { body: apiResponse } = await supertest + .post( + '/api/beats/agents_tags/removals' + ) + .set('kbn-xsrf', 'xxx') + .send({ + removals: [ + { beat_id: nonExistentBeatId, tag: 'production' } + ] + }) + .expect(200); + + expect(apiResponse.removals).to.eql([ + { status: 404, result: `Beat ${nonExistentBeatId} not found` } + ]); + }); + + it('should return errors for non-existent tags', async () => { + const nonExistentTag = chance.word(); + + const { body: apiResponse } = await supertest + .post( + '/api/beats/agents_tags/removals' + ) + .set('kbn-xsrf', 'xxx') + .send({ + removals: [ + { beat_id: 'bar', tag: nonExistentTag } + ] + }) + .expect(200); + + expect(apiResponse.removals).to.eql([ + { status: 404, result: `Tag ${nonExistentTag} not found` } + ]); + + const esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:bar` + }); + + const beat = esResponse._source.beat; + expect(beat).to.not.have.property('tags'); + }); + + it('should return errors for non-existent beats and tags', async () => { + const nonExistentBeatId = chance.word(); + const nonExistentTag = chance.word(); + + const { body: apiResponse } = await supertest + .post( + '/api/beats/agents_tags/removals' + ) + .set('kbn-xsrf', 'xxx') + .send({ + removals: [ + { beat_id: nonExistentBeatId, tag: nonExistentTag } + ] + }) + .expect(200); + + expect(apiResponse.removals).to.eql([ + { status: 404, result: `Beat ${nonExistentBeatId} and tag ${nonExistentTag} not found` } + ]); + + const esResponse = await es.get({ + index: ES_INDEX_NAME, + type: ES_TYPE_NAME, + id: `beat:bar` + }); + + const beat = esResponse._source.beat; + expect(beat).to.not.have.property('tags'); + }); + }); +} diff --git a/x-pack/test/functional/es_archives/beats/list/data.json.gz b/x-pack/test/functional/es_archives/beats/list/data.json.gz index 48094292337780ce8c10a685ddda5f9446160c5c..b33ecf434c104716138ed08200cf49feaa9b73c7 100644 GIT binary patch literal 447 zcmV;w0YLsAiwFqO9SBQOZ*BnHRLgGTFbur=D-53-!Ez);@^|#qVqs7c zoi!R;sUJahgZ%qSP6E5wCOveK0DCb&a)zLW93P@MPWoS4O!7Ff&LmGEv4hPJG6x^{ zuxc#s1Ax@fz#408`h`a5yAeL?P+VFBmJOKz%io9nCEK~7HB;{yHz3cb_#92B8Lq50 z_yOx{KV8=s)i#tV$;gthzp4$?C%SV)LraXS=a|#9)1YG#jKQuediRD+C@T;~DZsR} zlIc`PN!2Q)R48L5FrA#K$LR7sM#m^R+(l#!zyF=cHTG>~ZpfT@m6wE4!mdm0C%P{6 zH14NTQz$e5lm^hVCf!w=Q}b_4A8f8V1bTRC2)#l?qm9 z7_u^DLaP;@b9dNQWrenIBQ;B@T%>$K`7;%H#`C2lDq}BmN)PMKV_)NB+d4a#zVh5B z?=wi^ACEOld%r)DU*PDL8&(NByke8~*8aX&pL&!{{NnTZ%DQOZ*Bm^RLgGTFbur=D-53-!Ez);@^|d1MPX19 zoi!R;sUJbQLH>OuCxKmTlODQAE(S=>5Y!CmLlnnJ|FOj+j}z-m@)Qy~*bE_a@PQAj z#^OEzNDU3FvBsufXoS8S;j<3KrA1)bkO{E-eb`^Jof}#+^`3D9@{Eel(S(}e%4&n3 zu)g-&b$wB7Lz$9{ED8Ik+CY7xJ4ZCM#JGBnIZZnaIwrvw?7E_NZ`g#g0%4Q_OiL@7 zPKA_Itx`&bGFAf9$(eeLF5hExjH1I`MAq=<|A|~<-&W>^%$ZktNhl@ky3~H6>rzkS zeknDTGUFUI7b!5tq+Vr$3&T+<11QReO6_;(j#B?XbU$|vy{q3$`_RXq9V_DzLZ2|?0HV)S!C2LJ#d`O}C1 From 7338f130fef7975f9fbb0bb693c35ac541182e81 Mon Sep 17 00:00:00 2001 From: Matt Apperson Date: Tue, 26 Jun 2018 10:48:27 -0400 Subject: [PATCH 19/43] [Beats Management] Move to Ingest UI arch and initial TS effort (#20039) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Beats Management] Initial scaffolding for plugin (#18977) * Initial scaffolding for Beats plugin * Removing bits not (yet) necessary in initial scaffolding * [Beats Management] Install Beats index template on plugin init (#19072) * Install Beats index template on plugin init * Adding missing files * [Beats Management] APIs: Create enrollment tokens (#19018) * WIP checkin * Register API routes * Fixing typo in index name * Adding TODOs * Removing commented out license checking code that isn't yet implemented * Remove unnecessary async/await * Don't return until indices have been refreshed * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Adding TODO * Fixing variable name * Using a single index * Adding expiration date field * Adding test for expiration date field * Ignore non-existent index * Fixing logic in test * Creating constant for default enrollment tokens TTL value * Updating test * Fixing name of test file (#19100) * [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 * [Beats Management] APIs: List beats (#19086) * WIP checkin * Add API integration test * Converting to Jest test * WIP checkin * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Updating mapping * [Beats Management] APIs: Verify beats (#19103) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Fleshing out remaining tests * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Moving TODO comment to right file * Rename determine* helper functions to find* * Fixing assertions (#19194) * [Beats Management] APIs: Update beat (#19148) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Add API tests * Update template to allow version field for beat * Implement PUT /api/beats/agent/{beat ID} API * Make enroll beat code consistent with update beat code * Fixing minor typo in TODO comment * Allow version in request payload * Make sure beat is not updated in ES in error scenarios * Adding version as required field in Enroll Beat API payload * Using destructuring * Fixing rename that was accidentally reversed in conflict fixing * [Beats Management] APIs: take auth tokens via headers (#19210) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Fixing minor typo in TODO comment * Make "Enroll Beat" API take enrollment token via header instead of request body * Make "Update Beat" API take access token via header instead of request body * [Beats Management] APIs: Create configuration block (#19270) * WIP checkin * WIP checkin * Add API integration test * Converting to Jest test * Fixing API for default case + adding test for it * Fixing copy pasta typos * Fixing variable name * Using a single index * Implementing GET /api/beats/agents API * Creating POST /api/beats/agents/verify API * Refactoring: extracting out helper functions * Expanding TODO note so I won't forget :) * Fixing file name * Updating mapping * Fixing minor typo in TODO comment * Implementing POST /api/beats/configuration_blocks API * Removing unnecessary escaping * Fleshing out types + adding validation for them * Making output singular (was outputs) * Removing metricbeat.inputs * Revert implementation of `POST /api/beats/configuration_blocks` API (#19340) This API allowed the user to operate at a level of abstraction that is unnecessarily and dangerously too low. A better API would be at one level higher, where users can create, update, and delete tags (where a tag can contain multiple configuration blocks). * [Beats Management] APIs: Create or update tag (#19342) * Updating mappings * Implementing PUT /api/beats/tag/{tag} API * [Beats Management] Prevent timing attacks when checking auth tokens (#19363) * Using crypto.timingSafeEqual() for comparing auth tokens * Prevent subtler timing attack in token comparison function * Introduce random delay after we try to find token in ES to mitigate timing attack * Remove random delay * [Beats Management] APIs: Assign tag(s) to beat(s) (#19431) * Using crypto.timingSafeEqual() for comparing auth tokens * Introduce random delay after we try to find token in ES to mitigate timing attack * Rename "determine" to "find" * Remove random delay * Starting to implement POST /api/beats/beats_tags API * Changing API * Updating tests for changes to API * Updating ES archive * Renaming * Use destructuring * Moving start of script to own line to increase readability * Using destructuring * [Beats Management] APIs: Remove tag(s) from beat(s) (#19440) * Using crypto.timingSafeEqual() for comparing auth tokens * Introduce random delay after we try to find token in ES to mitigate timing attack * Remove random delay * Starting to implement POST /api/beats/beats_tags API * Changing API * Updating tests for changes to API * Renaming * Use destructuring * Using crypto.timingSafeEqual() for comparing auth tokens * Introduce random delay after we try to find token in ES to mitigate timing attack * Implementing `POST /api/beats/agents_tags/removals` API * Updating ES archive * Use destructuring * Moving start of script to own line to increase readability * Nothing to remove if there are no existing tags! * Updating tests to match changes in bulk update painless script * Use destructuring * Ported over base types and arch structure * move management of installIndexTemplate into the framework adapter * ts-lint fix * tslint fixes * more ts tweaks * fix paths * added several working endpoints * add more routes and bug fixes * fix linting * fix type remove CRUFT * remove more cruft * remove more CRUFT * added comments, change plurality * add tsconfig file * add extends path * fixed typo * serveral PR review fixes * fixed lodash type version * “fix” types by applying a lot of any --- package.json | 2 +- x-pack/package.json | 7 +- .../common/constants/configuration_blocks.js | 19 -- .../common/constants/configuration_blocks.ts | 15 + .../common/constants/{index.js => index.ts} | 5 +- .../{index_names.js => index_names.ts} | 2 +- .../common/constants/{plugin.js => plugin.ts} | 2 +- x-pack/plugins/beats/{index.js => index.ts} | 27 +- x-pack/plugins/beats/server/kibana.index.ts | 14 + .../beats/elasticsearch_beats_adapter.ts | 218 +++++++++++++++ .../kibana/kibana_framework_adapter.ts | 82 ++++++ .../tags/elasticsearch_tags_adapter.ts | 57 ++++ .../tokens/elasticsearch_tokens_adapter.ts | 83 ++++++ .../client/call_with_internal_user_factory.js | 16 -- .../lib/client/call_with_request_factory.js | 18 -- .../plugins/beats/server/lib/client/index.js | 8 - .../beats/server/lib/compose/kibana.ts | 45 +++ .../server/lib/crypto/are_tokens_equal.js | 21 -- .../plugins/beats/server/lib/domains/beats.ts | 259 ++++++++++++++++++ .../plugins/beats/server/lib/domains/tags.ts | 90 ++++++ .../beats/server/lib/domains/tokens.ts | 80 ++++++ .../index_template/install_index_template.js | 18 -- x-pack/plugins/beats/server/lib/lib.ts | 212 ++++++++++++++ .../plugins/beats/server/management_server.ts | 30 ++ .../beats/server/rest_api/beats/enroll.ts | 63 +++++ .../beats/server/rest_api/beats/list.ts | 23 ++ .../server/rest_api/beats/tag_assignment.ts | 48 ++++ .../server/rest_api/beats/tag_removal.ts | 48 ++++ .../beats/server/rest_api/beats/update.ts | 62 +++++ .../beats/server/rest_api/beats/verify.ts | 73 +++++ .../plugins/beats/server/rest_api/tags/set.ts | 57 ++++ .../beats/server/rest_api/tokens/create.ts | 42 +++ .../plugins/beats/server/routes/api/index.js | 25 -- .../register_assign_tags_to_beats_route.js | 169 ------------ ...register_create_enrollment_tokens_route.js | 70 ----- .../routes/api/register_enroll_beat_route.js | 115 -------- .../routes/api/register_list_beats_route.js | 47 ---- .../register_remove_tags_from_beats_route.js | 166 ----------- .../routes/api/register_set_tag_route.js | 124 --------- .../routes/api/register_update_beat_route.js | 101 ------- .../routes/api/register_verify_beats_route.js | 143 ---------- x-pack/plugins/beats/server/utils/README.md | 1 + .../error_wrappers/index.ts} | 0 .../error_wrappers/wrap_es_error.test.js | 5 +- .../error_wrappers/wrap_es_error.ts} | 6 +- .../server/utils/find_non_existent_items.ts | 14 + .../index_templates}/beats_template.json | 4 +- .../index_templates/index.ts} | 3 +- .../plugins/beats/server/utils/polyfills.ts | 17 ++ .../beats/server/utils/wrap_request.ts | 24 ++ x-pack/plugins/beats/tsconfig.json | 3 + .../lib/crypto/index.js => types/json.t.ts} | 5 +- x-pack/plugins/beats/wallaby.js | 27 ++ x-pack/yarn.lock | 30 +- yarn.lock | 17 +- 55 files changed, 1767 insertions(+), 1095 deletions(-) delete mode 100644 x-pack/plugins/beats/common/constants/configuration_blocks.js create mode 100644 x-pack/plugins/beats/common/constants/configuration_blocks.ts rename x-pack/plugins/beats/common/constants/{index.js => index.ts} (76%) rename x-pack/plugins/beats/common/constants/{index_names.js => index_names.ts} (90%) rename x-pack/plugins/beats/common/constants/{plugin.js => plugin.ts} (94%) rename x-pack/plugins/beats/{index.js => index.ts} (51%) create mode 100644 x-pack/plugins/beats/server/kibana.index.ts create mode 100644 x-pack/plugins/beats/server/lib/adapters/beats/elasticsearch_beats_adapter.ts create mode 100644 x-pack/plugins/beats/server/lib/adapters/famework/kibana/kibana_framework_adapter.ts create mode 100644 x-pack/plugins/beats/server/lib/adapters/tags/elasticsearch_tags_adapter.ts create mode 100644 x-pack/plugins/beats/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts delete mode 100644 x-pack/plugins/beats/server/lib/client/call_with_internal_user_factory.js delete mode 100644 x-pack/plugins/beats/server/lib/client/call_with_request_factory.js delete mode 100644 x-pack/plugins/beats/server/lib/client/index.js create mode 100644 x-pack/plugins/beats/server/lib/compose/kibana.ts delete mode 100644 x-pack/plugins/beats/server/lib/crypto/are_tokens_equal.js create mode 100644 x-pack/plugins/beats/server/lib/domains/beats.ts create mode 100644 x-pack/plugins/beats/server/lib/domains/tags.ts create mode 100644 x-pack/plugins/beats/server/lib/domains/tokens.ts delete mode 100644 x-pack/plugins/beats/server/lib/index_template/install_index_template.js create mode 100644 x-pack/plugins/beats/server/lib/lib.ts create mode 100644 x-pack/plugins/beats/server/management_server.ts create mode 100644 x-pack/plugins/beats/server/rest_api/beats/enroll.ts create mode 100644 x-pack/plugins/beats/server/rest_api/beats/list.ts create mode 100644 x-pack/plugins/beats/server/rest_api/beats/tag_assignment.ts create mode 100644 x-pack/plugins/beats/server/rest_api/beats/tag_removal.ts create mode 100644 x-pack/plugins/beats/server/rest_api/beats/update.ts create mode 100644 x-pack/plugins/beats/server/rest_api/beats/verify.ts create mode 100644 x-pack/plugins/beats/server/rest_api/tags/set.ts create mode 100644 x-pack/plugins/beats/server/rest_api/tokens/create.ts delete mode 100644 x-pack/plugins/beats/server/routes/api/index.js delete mode 100644 x-pack/plugins/beats/server/routes/api/register_assign_tags_to_beats_route.js delete mode 100644 x-pack/plugins/beats/server/routes/api/register_create_enrollment_tokens_route.js delete mode 100644 x-pack/plugins/beats/server/routes/api/register_enroll_beat_route.js delete mode 100644 x-pack/plugins/beats/server/routes/api/register_list_beats_route.js delete mode 100644 x-pack/plugins/beats/server/routes/api/register_remove_tags_from_beats_route.js delete mode 100644 x-pack/plugins/beats/server/routes/api/register_set_tag_route.js delete mode 100644 x-pack/plugins/beats/server/routes/api/register_update_beat_route.js delete mode 100644 x-pack/plugins/beats/server/routes/api/register_verify_beats_route.js create mode 100644 x-pack/plugins/beats/server/utils/README.md rename x-pack/plugins/beats/server/{lib/error_wrappers/index.js => utils/error_wrappers/index.ts} (100%) rename x-pack/plugins/beats/server/{lib => utils}/error_wrappers/wrap_es_error.test.js (90%) rename x-pack/plugins/beats/server/{lib/error_wrappers/wrap_es_error.js => utils/error_wrappers/wrap_es_error.ts} (79%) create mode 100644 x-pack/plugins/beats/server/utils/find_non_existent_items.ts rename x-pack/plugins/beats/server/{lib/index_template => utils/index_templates}/beats_template.json (97%) rename x-pack/plugins/beats/server/{lib/index_template/index.js => utils/index_templates/index.ts} (73%) create mode 100644 x-pack/plugins/beats/server/utils/polyfills.ts create mode 100644 x-pack/plugins/beats/server/utils/wrap_request.ts create mode 100644 x-pack/plugins/beats/tsconfig.json rename x-pack/plugins/beats/{server/lib/crypto/index.js => types/json.t.ts} (77%) create mode 100644 x-pack/plugins/beats/wallaby.js diff --git a/package.json b/package.json index ab046a29f32a1..0c3fd47acbefc 100644 --- a/package.json +++ b/package.json @@ -345,7 +345,7 @@ "tree-kill": "^1.1.0", "ts-jest": "^22.4.6", "ts-loader": "^3.5.0", - "ts-node": "^6.0.3", + "ts-node": "^6.1.1", "tslint": "^5.10.0", "tslint-config-prettier": "^1.12.0", "tslint-plugin-prettier": "^1.3.0", diff --git a/x-pack/package.json b/x-pack/package.json index b49af4fd67a89..555aed114a2c2 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -27,8 +27,12 @@ "@kbn/es": "link:../packages/kbn-es", "@kbn/plugin-helpers": "link:../packages/kbn-plugin-helpers", "@kbn/test": "link:../packages/kbn-test", + "@types/boom": "^4.3.8", + "@types/hapi": "15.0.1", "@types/jest": "^22.2.3", - "@types/pngjs": "^3.3.1", + "@types/joi": "^10.4.0", + "@types/lodash": "^3.10.0", + "@types/pngjs": "^3.3.0", "abab": "^1.0.4", "ansicolors": "0.3.2", "aws-sdk": "2.2.33", @@ -87,6 +91,7 @@ "@kbn/ui-framework": "link:../packages/kbn-ui-framework", "@samverschueren/stream-to-observable": "^0.3.0", "@slack/client": "^4.2.2", + "@types/uuid": "^3.4.3", "angular-paging": "2.2.1", "angular-resource": "1.4.9", "angular-sanitize": "1.4.9", diff --git a/x-pack/plugins/beats/common/constants/configuration_blocks.js b/x-pack/plugins/beats/common/constants/configuration_blocks.js deleted file mode 100644 index 1818b75335f3a..0000000000000 --- a/x-pack/plugins/beats/common/constants/configuration_blocks.js +++ /dev/null @@ -1,19 +0,0 @@ -/* - * 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/configuration_blocks.ts b/x-pack/plugins/beats/common/constants/configuration_blocks.ts new file mode 100644 index 0000000000000..e89e53e25b89d --- /dev/null +++ b/x-pack/plugins/beats/common/constants/configuration_blocks.ts @@ -0,0 +1,15 @@ +/* + * 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 enum ConfigurationBlockTypes { + FilebeatInputs = 'filebeat.inputs', + FilebeatModules = 'filebeat.modules', + MetricbeatModules = 'metricbeat.modules', + Output = 'output', + Processors = 'processors', +} + +export const UNIQUENESS_ENFORCING_TYPES = [ConfigurationBlockTypes.Output]; diff --git a/x-pack/plugins/beats/common/constants/index.js b/x-pack/plugins/beats/common/constants/index.ts similarity index 76% rename from x-pack/plugins/beats/common/constants/index.js rename to x-pack/plugins/beats/common/constants/index.ts index 77c41be579c33..4662865e208a7 100644 --- a/x-pack/plugins/beats/common/constants/index.js +++ b/x-pack/plugins/beats/common/constants/index.ts @@ -6,4 +6,7 @@ export { PLUGIN } from './plugin'; export { INDEX_NAMES } from './index_names'; -export { CONFIGURATION_BLOCKS } from './configuration_blocks'; +export { + UNIQUENESS_ENFORCING_TYPES, + ConfigurationBlockTypes, +} from './configuration_blocks'; diff --git a/x-pack/plugins/beats/common/constants/index_names.js b/x-pack/plugins/beats/common/constants/index_names.ts similarity index 90% rename from x-pack/plugins/beats/common/constants/index_names.js rename to x-pack/plugins/beats/common/constants/index_names.ts index e63e8b08a6ef4..f8d20fb79c360 100644 --- a/x-pack/plugins/beats/common/constants/index_names.js +++ b/x-pack/plugins/beats/common/constants/index_names.ts @@ -5,5 +5,5 @@ */ export const INDEX_NAMES = { - BEATS: '.management-beats' + BEATS: '.management-beats', }; diff --git a/x-pack/plugins/beats/common/constants/plugin.js b/x-pack/plugins/beats/common/constants/plugin.ts similarity index 94% rename from x-pack/plugins/beats/common/constants/plugin.js rename to x-pack/plugins/beats/common/constants/plugin.ts index 289bc488c58a6..ba12300075bf2 100644 --- a/x-pack/plugins/beats/common/constants/plugin.js +++ b/x-pack/plugins/beats/common/constants/plugin.ts @@ -5,5 +5,5 @@ */ export const PLUGIN = { - ID: 'beats' + ID: 'beats', }; diff --git a/x-pack/plugins/beats/index.js b/x-pack/plugins/beats/index.ts similarity index 51% rename from x-pack/plugins/beats/index.js rename to x-pack/plugins/beats/index.ts index c105813e36ff6..ce9b8147dbe4b 100644 --- a/x-pack/plugins/beats/index.js +++ b/x-pack/plugins/beats/index.ts @@ -4,24 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import { installIndexTemplate } from './server/lib/index_template'; -import { registerApiRoutes } from './server/routes/api'; +import Joi from 'joi'; import { PLUGIN } from './common/constants'; +import { initServerWithKibana } from './server/kibana.index'; const DEFAULT_ENROLLMENT_TOKENS_TTL_S = 10 * 60; // 10 minutes -export function beats(kibana) { +export function beats(kibana: any) { return new kibana.Plugin({ + config: () => + Joi.object({ + enabled: Joi.boolean().default(true), + enrollmentTokensTtlInSeconds: Joi.number() + .integer() + .min(1) + .default(DEFAULT_ENROLLMENT_TOKENS_TTL_S), + }).default(), + configPrefix: 'xpack.beats', id: PLUGIN.ID, require: ['kibana', 'elasticsearch', 'xpack_main'], - configPrefix: 'xpack.beats', - config: Joi => Joi.object({ - enabled: Joi.boolean().default(true), - enrollmentTokensTtlInSeconds: Joi.number().integer().min(1).default(DEFAULT_ENROLLMENT_TOKENS_TTL_S) - }).default(), - init: async function (server) { - await installIndexTemplate(server); - registerApiRoutes(server); - } + init(server: any) { + initServerWithKibana(server); + }, }); } diff --git a/x-pack/plugins/beats/server/kibana.index.ts b/x-pack/plugins/beats/server/kibana.index.ts new file mode 100644 index 0000000000000..c9bc9b8bf02f4 --- /dev/null +++ b/x-pack/plugins/beats/server/kibana.index.ts @@ -0,0 +1,14 @@ +/* + * 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 { Server } from 'hapi'; +import { compose } from './lib/compose/kibana'; +import { initManagementServer } from './management_server'; + +export const initServerWithKibana = (hapiServer: Server) => { + const libs = compose(hapiServer); + initManagementServer(libs); +}; diff --git a/x-pack/plugins/beats/server/lib/adapters/beats/elasticsearch_beats_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/beats/elasticsearch_beats_adapter.ts new file mode 100644 index 0000000000000..283f65c1258ae --- /dev/null +++ b/x-pack/plugins/beats/server/lib/adapters/beats/elasticsearch_beats_adapter.ts @@ -0,0 +1,218 @@ +/* + * 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 { flatten, get, omit } from 'lodash'; +import moment from 'moment'; +import { INDEX_NAMES } from '../../../../common/constants'; +import { + BackendFrameworkAdapter, + CMBeat, + CMBeatsAdapter, + CMTagAssignment, + FrameworkRequest, +} from '../../lib'; + +export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { + private framework: BackendFrameworkAdapter; + + constructor(framework: BackendFrameworkAdapter) { + this.framework = framework; + } + + public async get(id: string) { + const params = { + id: `beat:${id}`, + ignore: [404], + index: INDEX_NAMES.BEATS, + type: '_doc', + }; + + const response = await this.framework.callWithInternalUser('get', params); + if (!response.found) { + return null; + } + + return get(response, '_source.beat'); + } + + public async insert(beat: CMBeat) { + const body = { + beat, + type: 'beat', + }; + + const params = { + body, + id: `beat:${beat.id}`, + index: INDEX_NAMES.BEATS, + refresh: 'wait_for', + type: '_doc', + }; + await this.framework.callWithInternalUser('create', params); + } + + public async update(beat: CMBeat) { + const body = { + beat, + type: 'beat', + }; + + const params = { + body, + id: `beat:${beat.id}`, + index: INDEX_NAMES.BEATS, + refresh: 'wait_for', + type: '_doc', + }; + return await this.framework.callWithInternalUser('index', params); + } + + public async getWithIds(req: FrameworkRequest, beatIds: string[]) { + const ids = beatIds.map(beatId => `beat:${beatId}`); + + const params = { + _source: false, + body: { + ids, + }, + index: INDEX_NAMES.BEATS, + type: '_doc', + }; + const response = await this.framework.callWithRequest(req, 'mget', params); + return get(response, 'docs', []); + } + + // TODO merge with getBeatsWithIds + public async getVerifiedWithIds(req: FrameworkRequest, beatIds: string[]) { + const ids = beatIds.map(beatId => `beat:${beatId}`); + + const params = { + _sourceInclude: ['beat.id', 'beat.verified_on'], + body: { + ids, + }, + index: INDEX_NAMES.BEATS, + type: '_doc', + }; + const response = await this.framework.callWithRequest(req, 'mget', params); + return get(response, 'docs', []); + } + + public async verifyBeats(req: FrameworkRequest, beatIds: string[]) { + if (!Array.isArray(beatIds) || beatIds.length === 0) { + return []; + } + + const verifiedOn = moment().toJSON(); + const body = flatten( + beatIds.map(beatId => [ + { update: { _id: `beat:${beatId}` } }, + { doc: { beat: { verified_on: verifiedOn } } }, + ]) + ); + + const params = { + body, + index: INDEX_NAMES.BEATS, + refresh: 'wait_for', + type: '_doc', + }; + + const response = await this.framework.callWithRequest(req, 'bulk', params); + return get(response, 'items', []); + } + + public async getAll(req: FrameworkRequest) { + const params = { + index: INDEX_NAMES.BEATS, + q: 'type:beat', + type: '_doc', + }; + const response = await this.framework.callWithRequest( + req, + 'search', + params + ); + + const beats = get(response, 'hits.hits', []); + return beats.map((beat: any) => omit(beat._source.beat, ['access_token'])); + } + + public async removeTagsFromBeats( + req: FrameworkRequest, + removals: CMTagAssignment[] + ): Promise { + const body = flatten( + removals.map(({ beatId, tag }) => { + const script = + '' + + 'def beat = ctx._source.beat; ' + + 'if (beat.tags != null) { ' + + ' beat.tags.removeAll([params.tag]); ' + + '}'; + + return [ + { update: { _id: `beat:${beatId}` } }, + { script: { source: script, params: { tag } } }, + ]; + }) + ); + + const params = { + body, + index: INDEX_NAMES.BEATS, + refresh: 'wait_for', + type: '_doc', + }; + + const response = await this.framework.callWithRequest(req, 'bulk', params); + return get(response, 'items', []).map( + (item: any, resultIdx: number) => ({ + idxInRequest: removals[resultIdx].idxInRequest, + result: item.update.result, + status: item.update.status, + }) + ); + } + + public async assignTagsToBeats( + req: FrameworkRequest, + assignments: CMTagAssignment[] + ): Promise { + const body = flatten( + assignments.map(({ beatId, tag }) => { + const script = + '' + + 'def beat = ctx._source.beat; ' + + 'if (beat.tags == null) { ' + + ' beat.tags = []; ' + + '} ' + + 'if (!beat.tags.contains(params.tag)) { ' + + ' beat.tags.add(params.tag); ' + + '}'; + + return [ + { update: { _id: `beat:${beatId}` } }, + { script: { source: script, params: { tag } } }, + ]; + }) + ); + + const params = { + body, + index: INDEX_NAMES.BEATS, + refresh: 'wait_for', + type: '_doc', + }; + + const response = await this.framework.callWithRequest(req, 'bulk', params); + return get(response, 'items', []).map((item: any, resultIdx: any) => ({ + idxInRequest: assignments[resultIdx].idxInRequest, + result: item.update.result, + status: item.update.status, + })); + } +} diff --git a/x-pack/plugins/beats/server/lib/adapters/famework/kibana/kibana_framework_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/famework/kibana/kibana_framework_adapter.ts new file mode 100644 index 0000000000000..6fc2fc4853b03 --- /dev/null +++ b/x-pack/plugins/beats/server/lib/adapters/famework/kibana/kibana_framework_adapter.ts @@ -0,0 +1,82 @@ +/* + * 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 { + BackendFrameworkAdapter, + FrameworkRequest, + FrameworkRouteOptions, + WrappableRequest, +} from '../../../lib'; + +import { IStrictReply, Request, Server } from 'hapi'; +import { + internalFrameworkRequest, + wrapRequest, +} from '../../../../utils/wrap_request'; + +export class KibanaBackendFrameworkAdapter implements BackendFrameworkAdapter { + public version: string; + + private server: Server; + + constructor(hapiServer: Server) { + this.server = hapiServer; + this.version = hapiServer.plugins.kibana.status.plugin.version; + } + + public getSetting(settingPath: string) { + // TODO type check this properly + // @ts-ignore + return this.server.config().get(settingPath); + } + + public exposeStaticDir(urlPath: string, dir: string): void { + this.server.route({ + handler: { + directory: { + path: dir, + }, + }, + method: 'GET', + path: urlPath, + }); + } + + public registerRoute( + route: FrameworkRouteOptions + ) { + const wrappedHandler = (request: any, reply: IStrictReply) => + route.handler(wrapRequest(request), reply); + + this.server.route({ + config: route.config, + handler: wrappedHandler, + method: route.method, + path: route.path, + }); + } + + public installIndexTemplate(name: string, template: {}) { + return this.callWithInternalUser('indices.putTemplate', { + body: template, + name, + }); + } + + public async callWithInternalUser(esMethod: string, options: {}) { + const { elasticsearch } = this.server.plugins; + const { callWithInternalUser } = elasticsearch.getCluster('admin'); + return await callWithInternalUser(esMethod, options); + } + + public async callWithRequest(req: FrameworkRequest, ...rest: any[]) { + const internalRequest = req[internalFrameworkRequest]; + const { elasticsearch } = internalRequest.server.plugins; + const { callWithRequest } = elasticsearch.getCluster('data'); + const fields = await callWithRequest(internalRequest, ...rest); + return fields; + } +} diff --git a/x-pack/plugins/beats/server/lib/adapters/tags/elasticsearch_tags_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/tags/elasticsearch_tags_adapter.ts new file mode 100644 index 0000000000000..2293ba77677fd --- /dev/null +++ b/x-pack/plugins/beats/server/lib/adapters/tags/elasticsearch_tags_adapter.ts @@ -0,0 +1,57 @@ +/* + * 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 { get } from 'lodash'; +import { INDEX_NAMES } from '../../../../common/constants'; +import { + BackendFrameworkAdapter, + BeatTag, + CMTagsAdapter, + FrameworkRequest, +} from '../../lib'; + +export class ElasticsearchTagsAdapter implements CMTagsAdapter { + private framework: BackendFrameworkAdapter; + + constructor(framework: BackendFrameworkAdapter) { + this.framework = framework; + } + + public async getTagsWithIds(req: FrameworkRequest, tagIds: string[]) { + const ids = tagIds.map(tag => `tag:${tag}`); + + // TODO abstract to kibana adapter as the more generic getDocs + const params = { + _source: false, + body: { + ids, + }, + index: INDEX_NAMES.BEATS, + type: '_doc', + }; + const response = await this.framework.callWithRequest(req, 'mget', params); + return get(response, 'docs', []); + } + + public async upsertTag(req: FrameworkRequest, tag: BeatTag) { + const body = { + tag, + type: 'tag', + }; + + const params = { + body, + id: `tag:${tag.id}`, + index: INDEX_NAMES.BEATS, + refresh: 'wait_for', + type: '_doc', + }; + const response = await this.framework.callWithRequest(req, 'index', params); + + // TODO this is not something that works for TS... change this return type + return get(response, 'result'); + } +} diff --git a/x-pack/plugins/beats/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts new file mode 100644 index 0000000000000..c8969c7ab08d0 --- /dev/null +++ b/x-pack/plugins/beats/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts @@ -0,0 +1,83 @@ +/* + * 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 { flatten, get } from 'lodash'; +import { INDEX_NAMES } from '../../../../common/constants'; +import { + BackendFrameworkAdapter, + CMTokensAdapter, + EnrollmentToken, + FrameworkRequest, +} from '../../lib'; + +export class ElasticsearchTokensAdapter implements CMTokensAdapter { + private framework: BackendFrameworkAdapter; + + constructor(framework: BackendFrameworkAdapter) { + this.framework = framework; + } + + public async deleteEnrollmentToken(enrollmentToken: string) { + const params = { + id: `enrollment_token:${enrollmentToken}`, + index: INDEX_NAMES.BEATS, + type: '_doc', + }; + + return this.framework.callWithInternalUser('delete', params); + } + + public async getEnrollmentToken( + tokenString: string + ): Promise { + const params = { + id: `enrollment_token:${tokenString}`, + ignore: [404], + index: INDEX_NAMES.BEATS, + type: '_doc', + }; + + const response = await this.framework.callWithInternalUser('get', params); + const tokenDetails = get( + response, + '_source.enrollment_token', + { + expires_on: '0', + token: null, + } + ); + + // Elasticsearch might return fast if the token is not found. OR it might return fast + // if the token *is* found. Either way, an attacker could using a timing attack to figure + // out whether a token is valid or not. So we introduce a random delay in returning from + // this function to obscure the actual time it took for Elasticsearch to find the token. + const randomDelayInMs = 25 + Math.round(Math.random() * 200); // between 25 and 225 ms + return new Promise(resolve => + setTimeout(() => resolve(tokenDetails), randomDelayInMs) + ); + } + + public async upsertTokens(req: FrameworkRequest, tokens: EnrollmentToken[]) { + const body = flatten( + tokens.map(token => [ + { index: { _id: `enrollment_token:${token.token}` } }, + { + enrollment_token: token, + type: 'enrollment_token', + }, + ]) + ); + + const params = { + body, + index: INDEX_NAMES.BEATS, + refresh: 'wait_for', + type: '_doc', + }; + + await this.framework.callWithRequest(req, 'bulk', params); + } +} diff --git a/x-pack/plugins/beats/server/lib/client/call_with_internal_user_factory.js b/x-pack/plugins/beats/server/lib/client/call_with_internal_user_factory.js deleted file mode 100644 index 8b5dbed773430..0000000000000 --- a/x-pack/plugins/beats/server/lib/client/call_with_internal_user_factory.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * 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 { once } from 'lodash'; - -const callWithInternalUser = once((server) => { - const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin'); - return callWithInternalUser; -}); - -export const callWithInternalUserFactory = (server) => { - return callWithInternalUser(server); -}; diff --git a/x-pack/plugins/beats/server/lib/client/call_with_request_factory.js b/x-pack/plugins/beats/server/lib/client/call_with_request_factory.js deleted file mode 100644 index c81670ed0cdec..0000000000000 --- a/x-pack/plugins/beats/server/lib/client/call_with_request_factory.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * 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 { once } from 'lodash'; - -const callWithRequest = once((server) => { - const { callWithRequest } = server.plugins.elasticsearch.getCluster('admin'); - return callWithRequest; -}); - -export const callWithRequestFactory = (server, request) => { - return (...args) => { - return callWithRequest(server)(request, ...args); - }; -}; diff --git a/x-pack/plugins/beats/server/lib/client/index.js b/x-pack/plugins/beats/server/lib/client/index.js deleted file mode 100644 index cdeee091cc66f..0000000000000 --- a/x-pack/plugins/beats/server/lib/client/index.js +++ /dev/null @@ -1,8 +0,0 @@ -/* - * 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 { callWithRequestFactory } from './call_with_request_factory'; -export { callWithInternalUserFactory } from './call_with_internal_user_factory'; diff --git a/x-pack/plugins/beats/server/lib/compose/kibana.ts b/x-pack/plugins/beats/server/lib/compose/kibana.ts new file mode 100644 index 0000000000000..ff478646aea89 --- /dev/null +++ b/x-pack/plugins/beats/server/lib/compose/kibana.ts @@ -0,0 +1,45 @@ +/* + * 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 { ElasticsearchBeatsAdapter } from '../adapters/beats/elasticsearch_beats_adapter'; +import { ElasticsearchTagsAdapter } from '../adapters/tags/elasticsearch_tags_adapter'; +import { ElasticsearchTokensAdapter } from '../adapters/tokens/elasticsearch_tokens_adapter'; + +import { KibanaBackendFrameworkAdapter } from '../adapters/famework/kibana/kibana_framework_adapter'; + +import { CMBeatsDomain } from '../domains/beats'; +import { CMTagsDomain } from '../domains/tags'; +import { CMTokensDomain } from '../domains/tokens'; + +import { CMDomainLibs, CMServerLibs } from '../lib'; + +import { Server } from 'hapi'; + +export function compose(server: Server): CMServerLibs { + const framework = new KibanaBackendFrameworkAdapter(server); + + const tags = new CMTagsDomain(new ElasticsearchTagsAdapter(framework)); + const tokens = new CMTokensDomain(new ElasticsearchTokensAdapter(framework), { + framework, + }); + const beats = new CMBeatsDomain(new ElasticsearchBeatsAdapter(framework), { + tags, + tokens, + }); + + const domainLibs: CMDomainLibs = { + beats, + tags, + tokens, + }; + + const libs: CMServerLibs = { + framework, + ...domainLibs, + }; + + return libs; +} diff --git a/x-pack/plugins/beats/server/lib/crypto/are_tokens_equal.js b/x-pack/plugins/beats/server/lib/crypto/are_tokens_equal.js deleted file mode 100644 index a6ed171d30e5e..0000000000000 --- a/x-pack/plugins/beats/server/lib/crypto/are_tokens_equal.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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 { timingSafeEqual } from 'crypto'; - -const RANDOM_TOKEN_1 = 'b48c4bda384a40cb91c6eb9b8849e77f'; -const RANDOM_TOKEN_2 = '80a3819e3cd64f4399f1d4886be7a08b'; - -export function areTokensEqual(token1, token2) { - if ((typeof token1 !== 'string') || (typeof token2 !== 'string') || (token1.length !== token2.length)) { - // This prevents a more subtle timing attack where we know already the tokens aren't going to - // match but still we don't return fast. Instead we compare two pre-generated random tokens using - // the same comparison algorithm that we would use to compare two equal-length tokens. - return timingSafeEqual(Buffer.from(RANDOM_TOKEN_1, 'utf8'), Buffer.from(RANDOM_TOKEN_2, 'utf8')); - } - - return timingSafeEqual(Buffer.from(token1, 'utf8'), Buffer.from(token2, 'utf8')); -} diff --git a/x-pack/plugins/beats/server/lib/domains/beats.ts b/x-pack/plugins/beats/server/lib/domains/beats.ts new file mode 100644 index 0000000000000..c0d9ec704e2b1 --- /dev/null +++ b/x-pack/plugins/beats/server/lib/domains/beats.ts @@ -0,0 +1,259 @@ +/* + * 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. + */ + +/* + * 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 { uniq } from 'lodash'; +import uuid from 'uuid'; +import { findNonExistentItems } from '../../utils/find_non_existent_items'; + +import { + CMAssignmentReturn, + CMBeat, + CMBeatsAdapter, + CMDomainLibs, + CMRemovalReturn, + CMTagAssignment, + FrameworkRequest, +} from '../lib'; + +export class CMBeatsDomain { + private adapter: CMBeatsAdapter; + private tags: CMDomainLibs['tags']; + private tokens: CMDomainLibs['tokens']; + + constructor( + adapter: CMBeatsAdapter, + libs: { tags: CMDomainLibs['tags']; tokens: CMDomainLibs['tokens'] } + ) { + this.adapter = adapter; + this.tags = libs.tags; + this.tokens = libs.tokens; + } + + public async update( + beatId: string, + accessToken: string, + beatData: Partial + ) { + const beat = await this.adapter.get(beatId); + + // TODO make return type enum + if (beat === null) { + return 'beat-not-found'; + } + + const isAccessTokenValid = this.tokens.areTokensEqual( + beat.access_token, + accessToken + ); + if (!isAccessTokenValid) { + return 'invalid-access-token'; + } + const isBeatVerified = beat.hasOwnProperty('verified_on'); + if (!isBeatVerified) { + return 'beat-not-verified'; + } + + await this.adapter.update({ + ...beat, + ...beatData, + }); + } + + // TODO more strongly type this + public async enrollBeat( + beatId: string, + remoteAddress: string, + beat: Partial + ) { + // TODO move this to the token lib + const accessToken = uuid.v4().replace(/-/g, ''); + await this.adapter.insert({ + ...beat, + access_token: accessToken, + host_ip: remoteAddress, + id: beatId, + } as CMBeat); + return { accessToken }; + } + + public async removeTagsFromBeats( + req: FrameworkRequest, + removals: CMTagAssignment[] + ): Promise { + const beatIds = uniq(removals.map(removal => removal.beatId)); + const tagIds = uniq(removals.map(removal => removal.tag)); + + const response = { + removals: removals.map(() => ({ status: null })), + }; + + const beats = await this.adapter.getWithIds(req, beatIds); + const tags = await this.tags.getTagsWithIds(req, tagIds); + + // Handle assignments containing non-existing beat IDs or tags + const nonExistentBeatIds = findNonExistentItems(beats, beatIds); + const nonExistentTags = await findNonExistentItems(tags, tagIds); + + addNonExistentItemToResponse( + response, + removals, + nonExistentBeatIds, + nonExistentTags, + 'removals' + ); + + // TODO abstract this + const validRemovals = removals + .map((removal, idxInRequest) => ({ + beatId: removal.beatId, + idxInRequest, // so we can add the result of this removal to the correct place in the response + tag: removal.tag, + })) + .filter((removal, idx) => response.removals[idx].status === null); + + if (validRemovals.length > 0) { + const removalResults = await this.adapter.removeTagsFromBeats( + req, + validRemovals + ); + return addToResultsToResponse('removals', response, removalResults); + } + return response; + } + + public async getAllBeats(req: FrameworkRequest) { + return await this.adapter.getAll(req); + } + + // TODO cleanup return value, should return a status enum + public async verifyBeats(req: FrameworkRequest, beatIds: string[]) { + const beatsFromEs = await this.adapter.getVerifiedWithIds(req, beatIds); + + const nonExistentBeatIds = beatsFromEs.reduce( + (nonExistentIds: any, beatFromEs: any, idx: any) => { + if (!beatFromEs.found) { + nonExistentIds.push(beatIds[idx]); + } + return nonExistentIds; + }, + [] + ); + + const alreadyVerifiedBeatIds = beatsFromEs + .filter((beat: any) => beat.found) + .filter((beat: any) => beat._source.beat.hasOwnProperty('verified_on')) + .map((beat: any) => beat._source.beat.id); + + const toBeVerifiedBeatIds = beatsFromEs + .filter((beat: any) => beat.found) + .filter((beat: any) => !beat._source.beat.hasOwnProperty('verified_on')) + .map((beat: any) => beat._source.beat.id); + + const verifications = await this.adapter.verifyBeats( + req, + toBeVerifiedBeatIds + ); + return { + alreadyVerifiedBeatIds, + nonExistentBeatIds, + toBeVerifiedBeatIds, + verifications, + }; + } + + public async assignTagsToBeats( + req: FrameworkRequest, + assignments: CMTagAssignment[] + ): Promise { + const beatIds = uniq(assignments.map(assignment => assignment.beatId)); + const tagIds = uniq(assignments.map(assignment => assignment.tag)); + + const response = { + assignments: assignments.map(() => ({ status: null })), + }; + const beats = await this.adapter.getWithIds(req, beatIds); + const tags = await this.tags.getTagsWithIds(req, tagIds); + + // Handle assignments containing non-existing beat IDs or tags + const nonExistentBeatIds = findNonExistentItems(beats, beatIds); + const nonExistentTags = findNonExistentItems(tags, tagIds); + + // TODO break out back into route / function response + // TODO causes function to error if a beat or tag does not exist + addNonExistentItemToResponse( + response, + assignments, + nonExistentBeatIds, + nonExistentTags, + 'assignments' + ); + + // TODO abstract this + const validAssignments = assignments + .map((assignment, idxInRequest) => ({ + beatId: assignment.beatId, + idxInRequest, // so we can add the result of this assignment to the correct place in the response + tag: assignment.tag, + })) + .filter((assignment, idx) => response.assignments[idx].status === null); + + if (validAssignments.length > 0) { + const assignmentResults = await this.adapter.assignTagsToBeats( + req, + validAssignments + ); + + // TODO This should prob not mutate + return addToResultsToResponse('assignments', response, assignmentResults); + } + return response; + } +} + +// TODO abstract to the route, also the key arg is a temp fix +function addNonExistentItemToResponse( + response: any, + assignments: any, + nonExistentBeatIds: any, + nonExistentTags: any, + key: string +) { + assignments.forEach(({ beatId, tag }: CMTagAssignment, idx: any) => { + const isBeatNonExistent = nonExistentBeatIds.includes(beatId); + const isTagNonExistent = nonExistentTags.includes(tag); + + if (isBeatNonExistent && isTagNonExistent) { + response[key][idx].status = 404; + response[key][idx].result = `Beat ${beatId} and tag ${tag} not found`; + } else if (isBeatNonExistent) { + response[key][idx].status = 404; + response[key][idx].result = `Beat ${beatId} not found`; + } else if (isTagNonExistent) { + response[key][idx].status = 404; + response[key][idx].result = `Tag ${tag} not found`; + } + }); +} + +// TODO dont mutate response +function addToResultsToResponse( + key: string, + response: any, + assignmentResults: any +) { + assignmentResults.forEach((assignmentResult: any) => { + const { idxInRequest, status, result } = assignmentResult; + response[key][idxInRequest].status = status; + response[key][idxInRequest].result = result; + }); + return response; +} diff --git a/x-pack/plugins/beats/server/lib/domains/tags.ts b/x-pack/plugins/beats/server/lib/domains/tags.ts new file mode 100644 index 0000000000000..43bb8dfed15a1 --- /dev/null +++ b/x-pack/plugins/beats/server/lib/domains/tags.ts @@ -0,0 +1,90 @@ +/* + * 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 { intersection, uniq, values } from 'lodash'; +import { UNIQUENESS_ENFORCING_TYPES } from '../../../common/constants'; +import { CMTagsAdapter, ConfigurationBlock, FrameworkRequest } from '../lib'; +import { entries } from './../../utils/polyfills'; + +export class CMTagsDomain { + private adapter: CMTagsAdapter; + constructor(adapter: CMTagsAdapter) { + this.adapter = adapter; + } + + public async getTagsWithIds(req: FrameworkRequest, tagIds: string[]) { + return await this.adapter.getTagsWithIds(req, tagIds); + } + + public async saveTag( + req: FrameworkRequest, + tagId: string, + configs: ConfigurationBlock[] + ) { + const { isValid, message } = await this.validateConfigurationBlocks( + configs + ); + if (!isValid) { + return { isValid, result: message }; + } + + const tag = { + configuration_blocks: configs, + id: tagId, + }; + return { + isValid: true, + result: await this.adapter.upsertTag(req, tag), + }; + } + + private validateConfigurationBlocks(configurationBlocks: any) { + const types = uniq(configurationBlocks.map((block: any) => block.type)); + + // If none of the types in the given configuration blocks are uniqueness-enforcing, + // we don't need to perform any further validation checks. + const uniquenessEnforcingTypes = intersection( + types, + UNIQUENESS_ENFORCING_TYPES + ); + if (uniquenessEnforcingTypes.length === 0) { + return { isValid: true }; + } + + // Count the number of uniqueness-enforcing types in the given configuration blocks + const typeCountMap = configurationBlocks.reduce((map: any, block: any) => { + const { type } = block; + if (!uniquenessEnforcingTypes.includes(type)) { + return map; + } + + const count = map[type] || 0; + return { + ...map, + [type]: count + 1, + }; + }, {}); + + // If there is no more than one of any uniqueness-enforcing types in the given + // configuration blocks, we don't need to perform any further validation checks. + if (values(typeCountMap).filter(count => count > 1).length === 0) { + return { isValid: true }; + } + + const message = entries(typeCountMap) + .filter(([, count]) => count > 1) + .map( + ([type, count]) => + `Expected only one configuration block of type '${type}' but found ${count}` + ) + .join(' '); + + return { + isValid: false, + message, + }; + } +} diff --git a/x-pack/plugins/beats/server/lib/domains/tokens.ts b/x-pack/plugins/beats/server/lib/domains/tokens.ts new file mode 100644 index 0000000000000..6e55d78ecdcc8 --- /dev/null +++ b/x-pack/plugins/beats/server/lib/domains/tokens.ts @@ -0,0 +1,80 @@ +/* + * 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 { timingSafeEqual } from 'crypto'; +import moment from 'moment'; +import uuid from 'uuid'; +import { CMTokensAdapter, FrameworkRequest } from '../lib'; +import { BackendFrameworkAdapter } from '../lib'; + +const RANDOM_TOKEN_1 = 'b48c4bda384a40cb91c6eb9b8849e77f'; +const RANDOM_TOKEN_2 = '80a3819e3cd64f4399f1d4886be7a08b'; + +export class CMTokensDomain { + private adapter: CMTokensAdapter; + private framework: BackendFrameworkAdapter; + + constructor( + adapter: CMTokensAdapter, + libs: { framework: BackendFrameworkAdapter } + ) { + this.adapter = adapter; + this.framework = libs.framework; + } + + public async getEnrollmentToken(enrollmentToken: string) { + return await this.adapter.getEnrollmentToken(enrollmentToken); + } + + public async deleteEnrollmentToken(enrollmentToken: string) { + return await this.adapter.deleteEnrollmentToken(enrollmentToken); + } + + public areTokensEqual(token1: string, token2: string) { + if ( + typeof token1 !== 'string' || + typeof token2 !== 'string' || + token1.length !== token2.length + ) { + // This prevents a more subtle timing attack where we know already the tokens aren't going to + // match but still we don't return fast. Instead we compare two pre-generated random tokens using + // the same comparison algorithm that we would use to compare two equal-length tokens. + return timingSafeEqual( + Buffer.from(RANDOM_TOKEN_1, 'utf8'), + Buffer.from(RANDOM_TOKEN_2, 'utf8') + ); + } + + return timingSafeEqual( + Buffer.from(token1, 'utf8'), + Buffer.from(token2, 'utf8') + ); + } + + public async createEnrollmentTokens( + req: FrameworkRequest, + numTokens: number = 1 + ): Promise { + const tokens = []; + const enrollmentTokensTtlInSeconds = this.framework.getSetting( + 'xpack.beats.enrollmentTokensTtlInSeconds' + ); + const enrollmentTokenExpiration = moment() + .add(enrollmentTokensTtlInSeconds, 'seconds') + .toJSON(); + + while (tokens.length < numTokens) { + tokens.push({ + expires_on: enrollmentTokenExpiration, + token: uuid.v4().replace(/-/g, ''), + }); + } + + await this.adapter.upsertTokens(req, tokens); + + return tokens.map(token => token.token); + } +} diff --git a/x-pack/plugins/beats/server/lib/index_template/install_index_template.js b/x-pack/plugins/beats/server/lib/index_template/install_index_template.js deleted file mode 100644 index 01b080903ccac..0000000000000 --- a/x-pack/plugins/beats/server/lib/index_template/install_index_template.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * 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 beatsIndexTemplate from './beats_template'; -import { callWithInternalUserFactory } from '../client'; - -const TEMPLATE_NAME = 'beats-template'; - -export function installIndexTemplate(server) { - const callWithInternalUser = callWithInternalUserFactory(server); - return callWithInternalUser('indices.putTemplate', { - name: TEMPLATE_NAME, - body: beatsIndexTemplate - }); -} diff --git a/x-pack/plugins/beats/server/lib/lib.ts b/x-pack/plugins/beats/server/lib/lib.ts new file mode 100644 index 0000000000000..37d0a989e4cf5 --- /dev/null +++ b/x-pack/plugins/beats/server/lib/lib.ts @@ -0,0 +1,212 @@ +/* + * 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 { IRouteAdditionalConfigurationOptions, IStrictReply } from 'hapi'; +import { internalFrameworkRequest } from '../utils/wrap_request'; +import { CMBeatsDomain } from './domains/beats'; +import { CMTagsDomain } from './domains/tags'; +import { CMTokensDomain } from './domains/tokens'; + +import { ConfigurationBlockTypes } from '../../common/constants'; + +export interface CMDomainLibs { + beats: CMBeatsDomain; + tags: CMTagsDomain; + tokens: CMTokensDomain; +} + +export interface CMServerLibs extends CMDomainLibs { + framework: BackendFrameworkAdapter; +} + +interface CMReturnedTagAssignment { + status: number | null; + result?: string; +} + +export interface CMAssignmentReturn { + assignments: CMReturnedTagAssignment[]; +} + +export interface CMRemovalReturn { + removals: CMReturnedTagAssignment[]; +} + +export interface ConfigurationBlock { + type: ConfigurationBlockTypes; + block_yml: string; +} + +export interface CMBeat { + id: string; + access_token: string; + verified_on: string; + type: string; + version: string; + host_ip: string; + host_name: string; + ephemeral_id: string; + local_configuration_yml: string; + tags: string; + central_configuration_yml: string; + metadata: {}; +} + +export interface BeatTag { + id: string; + configuration_blocks: ConfigurationBlock[]; +} + +export interface EnrollmentToken { + token: string | null; + expires_on: string; +} + +export interface CMTokensAdapter { + deleteEnrollmentToken(enrollmentToken: string): Promise; + getEnrollmentToken(enrollmentToken: string): Promise; + upsertTokens(req: FrameworkRequest, tokens: EnrollmentToken[]): Promise; +} + +// FIXME: fix getTagsWithIds return type +export interface CMTagsAdapter { + getTagsWithIds(req: FrameworkRequest, tagIds: string[]): any; + upsertTag(req: FrameworkRequest, tag: BeatTag): Promise<{}>; +} + +// FIXME: fix getBeatsWithIds return type +export interface CMBeatsAdapter { + insert(beat: CMBeat): Promise; + update(beat: CMBeat): Promise; + get(id: string): any; + getAll(req: FrameworkRequest): any; + getWithIds(req: FrameworkRequest, beatIds: string[]): any; + getVerifiedWithIds(req: FrameworkRequest, beatIds: string[]): any; + verifyBeats(req: FrameworkRequest, beatIds: string[]): any; + removeTagsFromBeats( + req: FrameworkRequest, + removals: CMTagAssignment[] + ): Promise; + assignTagsToBeats( + req: FrameworkRequest, + assignments: CMTagAssignment[] + ): Promise; +} + +export interface CMTagAssignment { + beatId: string; + tag: string; + idxInRequest?: number; +} + +/** + * The following are generic types, sharable between projects + */ + +export interface BackendFrameworkAdapter { + version: string; + getSetting(settingPath: string): string | number; + exposeStaticDir(urlPath: string, dir: string): void; + installIndexTemplate(name: string, template: {}): void; + registerRoute( + route: FrameworkRouteOptions + ): void; + callWithInternalUser(esMethod: string, options: {}): Promise; + callWithRequest( + req: FrameworkRequest, + method: 'search', + options?: object + ): Promise>; + callWithRequest( + req: FrameworkRequest, + method: 'fieldCaps', + options?: object + ): Promise; + callWithRequest( + req: FrameworkRequest, + method: string, + options?: object + ): Promise; +} + +interface DatabaseFieldCapsResponse extends DatabaseResponse { + fields: FieldsResponse; +} + +export interface FieldsResponse { + [name: string]: FieldDef; +} + +export interface FieldDetails { + searchable: boolean; + aggregatable: boolean; + type: string; +} + +export interface FieldDef { + [type: string]: FieldDetails; +} + +export interface FrameworkRequest< + InternalRequest extends WrappableRequest = WrappableRequest +> { + [internalFrameworkRequest]: InternalRequest; + headers: InternalRequest['headers']; + info: InternalRequest['info']; + payload: InternalRequest['payload']; + params: InternalRequest['params']; + query: InternalRequest['query']; +} + +export interface FrameworkRouteOptions< + RouteRequest extends WrappableRequest, + RouteResponse +> { + path: string; + method: string | string[]; + vhost?: string; + handler: FrameworkRouteHandler; + config?: Pick< + IRouteAdditionalConfigurationOptions, + Exclude + >; +} + +export type FrameworkRouteHandler< + RouteRequest extends WrappableRequest, + RouteResponse +> = ( + request: FrameworkRequest, + reply: IStrictReply +) => void; + +export interface WrappableRequest< + Payload = any, + Params = any, + Query = any, + Headers = any, + Info = any +> { + headers: Headers; + info: Info; + payload: Payload; + params: Params; + query: Query; +} + +interface DatabaseResponse { + took: number; + timeout: boolean; +} + +interface DatabaseSearchResponse + extends DatabaseResponse { + aggregations?: Aggregations; + hits: { + total: number; + hits: Hit[]; + }; +} diff --git a/x-pack/plugins/beats/server/management_server.ts b/x-pack/plugins/beats/server/management_server.ts new file mode 100644 index 0000000000000..ed0917eda8ced --- /dev/null +++ b/x-pack/plugins/beats/server/management_server.ts @@ -0,0 +1,30 @@ +/* + * 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 { CMServerLibs } from './lib/lib'; +import { createBeatEnrollmentRoute } from './rest_api/beats/enroll'; +import { createListAgentsRoute } from './rest_api/beats/list'; +import { createTagAssignmentsRoute } from './rest_api/beats/tag_assignment'; +import { createTagRemovalsRoute } from './rest_api/beats/tag_removal'; +import { createBeatUpdateRoute } from './rest_api/beats/update'; +import { createBeatVerificationRoute } from './rest_api/beats/verify'; +import { createSetTagRoute } from './rest_api/tags/set'; +import { createTokensRoute } from './rest_api/tokens/create'; + +import { beatsIndexTemplate } from './utils/index_templates'; + +export const initManagementServer = (libs: CMServerLibs) => { + libs.framework.installIndexTemplate('beats-template', beatsIndexTemplate); + + libs.framework.registerRoute(createTagAssignmentsRoute(libs)); + libs.framework.registerRoute(createListAgentsRoute(libs)); + libs.framework.registerRoute(createTagRemovalsRoute(libs)); + libs.framework.registerRoute(createBeatEnrollmentRoute(libs)); + libs.framework.registerRoute(createSetTagRoute(libs)); + libs.framework.registerRoute(createTokensRoute(libs)); + libs.framework.registerRoute(createBeatVerificationRoute(libs)); + libs.framework.registerRoute(createBeatUpdateRoute(libs)); +}; diff --git a/x-pack/plugins/beats/server/rest_api/beats/enroll.ts b/x-pack/plugins/beats/server/rest_api/beats/enroll.ts new file mode 100644 index 0000000000000..fe154592564ae --- /dev/null +++ b/x-pack/plugins/beats/server/rest_api/beats/enroll.ts @@ -0,0 +1,63 @@ +/* + * 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 { omit } from 'lodash'; +import moment from 'moment'; +import { CMServerLibs } from '../../lib/lib'; +import { wrapEsError } from '../../utils/error_wrappers'; + +// TODO: add license check pre-hook +// TODO: write to Kibana audit log file +export const createBeatEnrollmentRoute = (libs: CMServerLibs) => ({ + config: { + auth: false, + validate: { + headers: Joi.object({ + 'kbn-beats-enrollment-token': Joi.string().required(), + }).options({ + allowUnknown: true, + }), + payload: Joi.object({ + host_name: Joi.string().required(), + type: Joi.string().required(), + version: Joi.string().required(), + }).required(), + }, + }, + handler: async (request: any, reply: any) => { + const { beatId } = request.params; + const enrollmentToken = request.headers['kbn-beats-enrollment-token']; + + try { + const { + token, + expires_on: expiresOn, + } = await libs.tokens.getEnrollmentToken(enrollmentToken); + + if (!token) { + return reply({ message: 'Invalid enrollment token' }).code(400); + } + if (moment(expiresOn).isBefore(moment())) { + return reply({ message: 'Expired enrollment token' }).code(400); + } + const { accessToken } = await libs.beats.enrollBeat( + beatId, + request.info.remoteAddress, + omit(request.payload, 'enrollment_token') + ); + + await libs.tokens.deleteEnrollmentToken(enrollmentToken); + + reply({ access_token: accessToken }).code(201); + } catch (err) { + // TODO move this to kibana route thing in adapter + return reply(wrapEsError(err)); + } + }, + method: 'POST', + path: '/api/beats/agent/{beatId}', +}); diff --git a/x-pack/plugins/beats/server/rest_api/beats/list.ts b/x-pack/plugins/beats/server/rest_api/beats/list.ts new file mode 100644 index 0000000000000..8263d1c0ff63f --- /dev/null +++ b/x-pack/plugins/beats/server/rest_api/beats/list.ts @@ -0,0 +1,23 @@ +/* + * 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 { CMServerLibs } from '../../lib/lib'; +import { wrapEsError } from '../../utils/error_wrappers'; + +// TODO: add license check pre-hook +export const createListAgentsRoute = (libs: CMServerLibs) => ({ + handler: async (request: any, reply: any) => { + try { + const beats = await libs.beats.getAllBeats(request); + reply({ beats }); + } catch (err) { + // TODO move this to kibana route thing in adapter + return reply(wrapEsError(err)); + } + }, + method: 'GET', + path: '/api/beats/agents', +}); diff --git a/x-pack/plugins/beats/server/rest_api/beats/tag_assignment.ts b/x-pack/plugins/beats/server/rest_api/beats/tag_assignment.ts new file mode 100644 index 0000000000000..d06c016ce6d12 --- /dev/null +++ b/x-pack/plugins/beats/server/rest_api/beats/tag_assignment.ts @@ -0,0 +1,48 @@ +/* + * 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 { CMServerLibs } from '../../lib/lib'; +import { wrapEsError } from '../../utils/error_wrappers'; + +// TODO: add license check pre-hook +// TODO: write to Kibana audit log file +export const createTagAssignmentsRoute = (libs: CMServerLibs) => ({ + config: { + validate: { + payload: Joi.object({ + assignments: Joi.array().items( + Joi.object({ + beat_id: Joi.string().required(), + tag: Joi.string().required(), + }) + ), + }).required(), + }, + }, + handler: async (request: any, reply: any) => { + const { assignments } = request.payload; + + // TODO abstract or change API to keep beatId consistent + const tweakedAssignments = assignments.map((assignment: any) => ({ + beatId: assignment.beat_id, + tag: assignment.tag, + })); + + try { + const response = await libs.beats.assignTagsToBeats( + request, + tweakedAssignments + ); + reply(response); + } catch (err) { + // TODO move this to kibana route thing in adapter + return reply(wrapEsError(err)); + } + }, + method: 'POST', + path: '/api/beats/agents_tags/assignments', +}); diff --git a/x-pack/plugins/beats/server/rest_api/beats/tag_removal.ts b/x-pack/plugins/beats/server/rest_api/beats/tag_removal.ts new file mode 100644 index 0000000000000..4da33dbd50cfc --- /dev/null +++ b/x-pack/plugins/beats/server/rest_api/beats/tag_removal.ts @@ -0,0 +1,48 @@ +/* + * 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 { CMServerLibs } from '../../lib/lib'; +import { wrapEsError } from '../../utils/error_wrappers'; + +// TODO: add license check pre-hook +// TODO: write to Kibana audit log file +export const createTagRemovalsRoute = (libs: CMServerLibs) => ({ + config: { + validate: { + payload: Joi.object({ + removals: Joi.array().items( + Joi.object({ + beat_id: Joi.string().required(), + tag: Joi.string().required(), + }) + ), + }).required(), + }, + }, + handler: async (request: any, reply: any) => { + const { removals } = request.payload; + + // TODO abstract or change API to keep beatId consistent + const tweakedRemovals = removals.map((removal: any) => ({ + beatId: removal.beat_id, + tag: removal.tag, + })); + + try { + const response = await libs.beats.removeTagsFromBeats( + request, + tweakedRemovals + ); + reply(response); + } catch (err) { + // TODO move this to kibana route thing in adapter + return reply(wrapEsError(err)); + } + }, + method: 'POST', + path: '/api/beats/agents_tags/removals', +}); diff --git a/x-pack/plugins/beats/server/rest_api/beats/update.ts b/x-pack/plugins/beats/server/rest_api/beats/update.ts new file mode 100644 index 0000000000000..41d403399d45f --- /dev/null +++ b/x-pack/plugins/beats/server/rest_api/beats/update.ts @@ -0,0 +1,62 @@ +/* + * 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 { CMServerLibs } from '../../lib/lib'; +import { wrapEsError } from '../../utils/error_wrappers'; + +// TODO: add license check pre-hook +// TODO: write to Kibana audit log file (include who did the verification as well) +export const createBeatUpdateRoute = (libs: CMServerLibs) => ({ + config: { + auth: false, + validate: { + headers: Joi.object({ + 'kbn-beats-access-token': Joi.string().required(), + }).options({ + allowUnknown: true, + }), + params: Joi.object({ + beatId: Joi.string(), + }), + payload: Joi.object({ + ephemeral_id: Joi.string(), + host_name: Joi.string(), + local_configuration_yml: Joi.string(), + metadata: Joi.object(), + type: Joi.string(), + version: Joi.string(), + }).required(), + }, + }, + handler: async (request: any, reply: any) => { + const { beatId } = request.params; + const accessToken = request.headers['kbn-beats-access-token']; + const remoteAddress = request.info.remoteAddress; + + try { + const status = await libs.beats.update(beatId, accessToken, { + ...request.payload, + host_ip: remoteAddress, + }); + + switch (status) { + case 'beat-not-found': + return reply({ message: 'Beat not found' }).code(404); + case 'invalid-access-token': + return reply({ message: 'Invalid access token' }).code(401); + case 'beat-not-verified': + return reply({ message: 'Beat has not been verified' }).code(400); + } + + reply().code(204); + } catch (err) { + return reply(wrapEsError(err)); + } + }, + method: 'PUT', + path: '/api/beats/agent/{beatId}', +}); diff --git a/x-pack/plugins/beats/server/rest_api/beats/verify.ts b/x-pack/plugins/beats/server/rest_api/beats/verify.ts new file mode 100644 index 0000000000000..866fa77d0c337 --- /dev/null +++ b/x-pack/plugins/beats/server/rest_api/beats/verify.ts @@ -0,0 +1,73 @@ +/* + * 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 { CMServerLibs } from '../../lib/lib'; +import { wrapEsError } from '../../utils/error_wrappers'; + +// TODO: add license check pre-hook +// TODO: write to Kibana audit log file +export const createBeatVerificationRoute = (libs: CMServerLibs) => ({ + config: { + auth: false, + validate: { + payload: Joi.object({ + beats: Joi.array() + .items({ + id: Joi.string().required(), + }) + .min(1), + }).required(), + }, + }, + handler: async (request: any, reply: any) => { + const beats = [...request.payload.beats]; + const beatIds = beats.map(beat => beat.id); + + try { + const { + verifications, + alreadyVerifiedBeatIds, + toBeVerifiedBeatIds, + nonExistentBeatIds, + } = await libs.beats.verifyBeats(request, beatIds); + + const verifiedBeatIds = verifications.reduce( + (verifiedBeatList: any, verification: any, idx: any) => { + if (verification.update.status === 200) { + verifiedBeatList.push(toBeVerifiedBeatIds[idx]); + } + return verifiedBeatList; + }, + [] + ); + + // TODO calculation of status should be done in-lib, w/switch statement here + beats.forEach(beat => { + if (nonExistentBeatIds.includes(beat.id)) { + beat.status = 404; + beat.result = 'not found'; + } else if (alreadyVerifiedBeatIds.includes(beat.id)) { + beat.status = 200; + beat.result = 'already verified'; + } else if (verifiedBeatIds.includes(beat.id)) { + beat.status = 200; + beat.result = 'verified'; + } else { + beat.status = 400; + beat.result = 'not verified'; + } + }); + + const response = { beats }; + reply(response); + } catch (err) { + return reply(wrapEsError(err)); + } + }, + method: 'POST', + path: '/api/beats/agents/verify', +}); diff --git a/x-pack/plugins/beats/server/rest_api/tags/set.ts b/x-pack/plugins/beats/server/rest_api/tags/set.ts new file mode 100644 index 0000000000000..3f7e579bd91ae --- /dev/null +++ b/x-pack/plugins/beats/server/rest_api/tags/set.ts @@ -0,0 +1,57 @@ +/* + * 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 { get, values } from 'lodash'; +import { ConfigurationBlockTypes } from '../../../common/constants'; +import { CMServerLibs } from '../../lib/lib'; +import { wrapEsError } from '../../utils/error_wrappers'; + +// TODO: add license check pre-hook +// TODO: write to Kibana audit log file +export const createSetTagRoute = (libs: CMServerLibs) => ({ + config: { + validate: { + params: Joi.object({ + tag: Joi.string(), + }), + payload: Joi.object({ + configuration_blocks: Joi.array().items( + Joi.object({ + block_yml: Joi.string().required(), + type: Joi.string() + .only(values(ConfigurationBlockTypes)) + .required(), + }) + ), + }).allow(null), + }, + }, + handler: async (request: any, reply: any) => { + const configurationBlocks = get( + request, + 'payload.configuration_blocks', + [] + ); + try { + const { isValid, result } = await libs.tags.saveTag( + request, + request.params.tag, + configurationBlocks + ); + if (!isValid) { + return reply({ result }).code(400); + } + + reply().code(result === 'created' ? 201 : 200); + } catch (err) { + // TODO move this to kibana route thing in adapter + return reply(wrapEsError(err)); + } + }, + method: 'PUT', + path: '/api/beats/tag/{tag}', +}); diff --git a/x-pack/plugins/beats/server/rest_api/tokens/create.ts b/x-pack/plugins/beats/server/rest_api/tokens/create.ts new file mode 100644 index 0000000000000..b4f3e2c1a6246 --- /dev/null +++ b/x-pack/plugins/beats/server/rest_api/tokens/create.ts @@ -0,0 +1,42 @@ +/* + * 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 { get } from 'lodash'; +import { CMServerLibs } from '../../lib/lib'; +import { wrapEsError } from '../../utils/error_wrappers'; + +// TODO: add license check pre-hook +// TODO: write to Kibana audit log file +const DEFAULT_NUM_TOKENS = 1; +export const createTokensRoute = (libs: CMServerLibs) => ({ + config: { + validate: { + payload: Joi.object({ + num_tokens: Joi.number() + .optional() + .default(DEFAULT_NUM_TOKENS) + .min(1), + }).allow(null), + }, + }, + handler: async (request: any, reply: any) => { + const numTokens = get(request, 'payload.num_tokens', DEFAULT_NUM_TOKENS); + + try { + const tokens = await libs.tokens.createEnrollmentTokens( + request, + numTokens + ); + reply({ tokens }); + } catch (err) { + // TODO move this to kibana route thing in adapter + return reply(wrapEsError(err)); + } + }, + method: 'POST', + path: '/api/beats/enrollment_tokens', +}); diff --git a/x-pack/plugins/beats/server/routes/api/index.js b/x-pack/plugins/beats/server/routes/api/index.js deleted file mode 100644 index 6ec0ad737352a..0000000000000 --- a/x-pack/plugins/beats/server/routes/api/index.js +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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 { registerCreateEnrollmentTokensRoute } from './register_create_enrollment_tokens_route'; -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 { registerSetTagRoute } from './register_set_tag_route'; -import { registerAssignTagsToBeatsRoute } from './register_assign_tags_to_beats_route'; -import { registerRemoveTagsFromBeatsRoute } from './register_remove_tags_from_beats_route'; - -export function registerApiRoutes(server) { - registerCreateEnrollmentTokensRoute(server); - registerEnrollBeatRoute(server); - registerListBeatsRoute(server); - registerVerifyBeatsRoute(server); - registerUpdateBeatRoute(server); - registerSetTagRoute(server); - registerAssignTagsToBeatsRoute(server); - registerRemoveTagsFromBeatsRoute(server); -} diff --git a/x-pack/plugins/beats/server/routes/api/register_assign_tags_to_beats_route.js b/x-pack/plugins/beats/server/routes/api/register_assign_tags_to_beats_route.js deleted file mode 100644 index 5f6c5f7a7b906..0000000000000 --- a/x-pack/plugins/beats/server/routes/api/register_assign_tags_to_beats_route.js +++ /dev/null @@ -1,169 +0,0 @@ -/* - * 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 { - get, - flatten, - uniq -} from 'lodash'; -import { INDEX_NAMES } from '../../../common/constants'; -import { callWithRequestFactory } from '../../lib/client'; -import { wrapEsError } from '../../lib/error_wrappers'; - -async function getDocs(callWithRequest, ids) { - const params = { - index: INDEX_NAMES.BEATS, - type: '_doc', - body: { ids }, - _source: false - }; - - const response = await callWithRequest('mget', params); - return get(response, 'docs', []); -} - -function getBeats(callWithRequest, beatIds) { - const ids = beatIds.map(beatId => `beat:${beatId}`); - return getDocs(callWithRequest, ids); -} - -function getTags(callWithRequest, tags) { - const ids = tags.map(tag => `tag:${tag}`); - return getDocs(callWithRequest, ids); -} - -async function findNonExistentItems(callWithRequest, items, getFn) { - const itemsFromEs = await getFn.call(null, callWithRequest, items); - return itemsFromEs.reduce((nonExistentItems, itemFromEs, idx) => { - if (!itemFromEs.found) { - nonExistentItems.push(items[idx]); - } - return nonExistentItems; - }, []); -} - -function findNonExistentBeatIds(callWithRequest, beatIds) { - return findNonExistentItems(callWithRequest, beatIds, getBeats); -} - -function findNonExistentTags(callWithRequest, tags) { - return findNonExistentItems(callWithRequest, tags, getTags); -} - -async function persistAssignments(callWithRequest, assignments) { - const body = flatten(assignments.map(({ beatId, tag }) => { - const script = '' - + 'def beat = ctx._source.beat; ' - + 'if (beat.tags == null) { ' - + ' beat.tags = []; ' - + '} ' - + 'if (!beat.tags.contains(params.tag)) { ' - + ' beat.tags.add(params.tag); ' - + '}'; - - return [ - { update: { _id: `beat:${beatId}` } }, - { script: { source: script, params: { tag } } } - ]; - })); - - const params = { - index: INDEX_NAMES.BEATS, - type: '_doc', - body, - refresh: 'wait_for' - }; - - const response = await callWithRequest('bulk', params); - return get(response, 'items', []) - .map((item, resultIdx) => ({ - status: item.update.status, - result: item.update.result, - idxInRequest: assignments[resultIdx].idxInRequest - })); -} - -function addNonExistentItemAssignmentsToResponse(response, assignments, nonExistentBeatIds, nonExistentTags) { - assignments.forEach(({ beat_id: beatId, tag }, idx) => { - const isBeatNonExistent = nonExistentBeatIds.includes(beatId); - const isTagNonExistent = nonExistentTags.includes(tag); - - if (isBeatNonExistent && isTagNonExistent) { - response.assignments[idx].status = 404; - response.assignments[idx].result = `Beat ${beatId} and tag ${tag} not found`; - } else if (isBeatNonExistent) { - response.assignments[idx].status = 404; - response.assignments[idx].result = `Beat ${beatId} not found`; - } else if (isTagNonExistent) { - response.assignments[idx].status = 404; - response.assignments[idx].result = `Tag ${tag} not found`; - } - }); -} - -function addAssignmentResultsToResponse(response, assignmentResults) { - assignmentResults.forEach(assignmentResult => { - const { idxInRequest, status, result } = assignmentResult; - response.assignments[idxInRequest].status = status; - response.assignments[idxInRequest].result = result; - }); -} - -// TODO: add license check pre-hook -// TODO: write to Kibana audit log file -export function registerAssignTagsToBeatsRoute(server) { - server.route({ - method: 'POST', - path: '/api/beats/agents_tags/assignments', - config: { - validate: { - payload: Joi.object({ - assignments: Joi.array().items(Joi.object({ - beat_id: Joi.string().required(), - tag: Joi.string().required() - })) - }).required() - } - }, - handler: async (request, reply) => { - const callWithRequest = callWithRequestFactory(server, request); - - const { assignments } = request.payload; - const beatIds = uniq(assignments.map(assignment => assignment.beat_id)); - const tags = uniq(assignments.map(assignment => assignment.tag)); - - const response = { - assignments: assignments.map(() => ({ status: null })) - }; - - try { - // Handle assignments containing non-existing beat IDs or tags - const nonExistentBeatIds = await findNonExistentBeatIds(callWithRequest, beatIds); - const nonExistentTags = await findNonExistentTags(callWithRequest, tags); - - addNonExistentItemAssignmentsToResponse(response, assignments, nonExistentBeatIds, nonExistentTags); - - const validAssignments = assignments - .map((assignment, idxInRequest) => ({ - beatId: assignment.beat_id, - tag: assignment.tag, - idxInRequest // so we can add the result of this assignment to the correct place in the response - })) - .filter((assignment, idx) => response.assignments[idx].status === null); - - if (validAssignments.length > 0) { - const assignmentResults = await persistAssignments(callWithRequest, validAssignments); - addAssignmentResultsToResponse(response, assignmentResults); - } - } catch (err) { - return reply(wrapEsError(err)); - } - - reply(response); - } - }); -} diff --git a/x-pack/plugins/beats/server/routes/api/register_create_enrollment_tokens_route.js b/x-pack/plugins/beats/server/routes/api/register_create_enrollment_tokens_route.js deleted file mode 100644 index 87ae30cd0e532..0000000000000 --- a/x-pack/plugins/beats/server/routes/api/register_create_enrollment_tokens_route.js +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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, - flatten -} from 'lodash'; -import { INDEX_NAMES } from '../../../common/constants'; -import { callWithRequestFactory } from '../../lib/client'; -import { wrapEsError } from '../../lib/error_wrappers'; - -function persistTokens(callWithRequest, tokens, enrollmentTokensTtlInSeconds) { - const enrollmentTokenExpiration = moment().add(enrollmentTokensTtlInSeconds, 'seconds').toJSON(); - const body = flatten(tokens.map(token => [ - { index: { _id: `enrollment_token:${token}` } }, - { type: 'enrollment_token', enrollment_token: { token, expires_on: enrollmentTokenExpiration } } - ])); - - const params = { - index: INDEX_NAMES.BEATS, - type: '_doc', - body, - refresh: 'wait_for' - }; - - return callWithRequest('bulk', params); -} - -// TODO: add license check pre-hook -// TODO: write to Kibana audit log file -export function registerCreateEnrollmentTokensRoute(server) { - const DEFAULT_NUM_TOKENS = 1; - const enrollmentTokensTtlInSeconds = server.config().get('xpack.beats.enrollmentTokensTtlInSeconds'); - - server.route({ - method: 'POST', - path: '/api/beats/enrollment_tokens', - config: { - validate: { - payload: Joi.object({ - num_tokens: Joi.number().optional().default(DEFAULT_NUM_TOKENS).min(1) - }).allow(null) - } - }, - handler: async (request, reply) => { - const callWithRequest = callWithRequestFactory(server, request); - const numTokens = get(request, 'payload.num_tokens', DEFAULT_NUM_TOKENS); - - const tokens = []; - while (tokens.length < numTokens) { - tokens.push(uuid.v4().replace(/-/g, "")); - } - - try { - await persistTokens(callWithRequest, tokens, enrollmentTokensTtlInSeconds); - } catch (err) { - return reply(wrapEsError(err)); - } - - const response = { tokens }; - reply(response); - } - }); -} diff --git a/x-pack/plugins/beats/server/routes/api/register_enroll_beat_route.js b/x-pack/plugins/beats/server/routes/api/register_enroll_beat_route.js deleted file mode 100644 index bad28c0ab9be5..0000000000000 --- a/x-pack/plugins/beats/server/routes/api/register_enroll_beat_route.js +++ /dev/null @@ -1,115 +0,0 @@ -/* - * 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); - const token = get(response, '_source.enrollment_token', {}); - - // Elasticsearch might return fast if the token is not found. OR it might return fast - // if the token *is* found. Either way, an attacker could using a timing attack to figure - // out whether a token is valid or not. So we introduce a random delay in returning from - // this function to obscure the actual time it took for Elasticsearch to find the token. - const randomDelayInMs = 25 + Math.round(Math.random() * 200); // between 25 and 225 ms - return new Promise(resolve => setTimeout(() => resolve(token), randomDelayInMs)); -} - -function deleteUsedEnrollmentToken(callWithInternalUser, enrollmentToken) { - const params = { - index: INDEX_NAMES.BEATS, - type: '_doc', - id: `enrollment_token:${enrollmentToken}` - }; - - return callWithInternalUser('delete', params); -} - -function persistBeat(callWithInternalUser, beat) { - const body = { - type: 'beat', - beat - }; - - const params = { - index: INDEX_NAMES.BEATS, - type: '_doc', - id: `beat:${beat.id}`, - 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({ - type: Joi.string().required(), - version: Joi.string().required(), - host_name: Joi.string().required() - }).required(), - headers: Joi.object({ - 'kbn-beats-enrollment-token': Joi.string().required() - }).options({ allowUnknown: true }) - }, - auth: false - }, - handler: async (request, reply) => { - const callWithInternalUser = callWithInternalUserFactory(server); - const { beatId } = request.params; - let accessToken; - - try { - const enrollmentToken = request.headers['kbn-beats-enrollment-token']; - const { token, expires_on: expiresOn } = await getEnrollmentToken(callWithInternalUser, enrollmentToken); - if (!token) { - 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, { - ...omit(request.payload, 'enrollment_token'), - id: beatId, - access_token: accessToken, - host_ip: remoteAddress - }); - - await deleteUsedEnrollmentToken(callWithInternalUser, enrollmentToken); - } catch (err) { - return reply(wrapEsError(err)); - } - - const response = { access_token: accessToken }; - reply(response).code(201); - } - }); -} diff --git a/x-pack/plugins/beats/server/routes/api/register_list_beats_route.js b/x-pack/plugins/beats/server/routes/api/register_list_beats_route.js deleted file mode 100644 index b84210988978f..0000000000000 --- a/x-pack/plugins/beats/server/routes/api/register_list_beats_route.js +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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 { - get, - omit -} from "lodash"; -import { INDEX_NAMES } from "../../../common/constants"; -import { callWithRequestFactory } from '../../lib/client'; -import { wrapEsError } from "../../lib/error_wrappers"; - -async function getBeats(callWithRequest) { - const params = { - index: INDEX_NAMES.BEATS, - type: '_doc', - q: 'type:beat' - }; - - const response = await callWithRequest('search', params); - return get(response, 'hits.hits', []); -} - -// TODO: add license check pre-hook -export function registerListBeatsRoute(server) { - server.route({ - method: 'GET', - path: '/api/beats/agents', - handler: async (request, reply) => { - const callWithRequest = callWithRequestFactory(server, request); - let beats; - - try { - beats = await getBeats(callWithRequest); - } catch (err) { - return reply(wrapEsError(err)); - } - - const response = { - beats: beats.map(beat => omit(beat._source.beat, ['access_token'])) - }; - reply(response); - } - }); -} diff --git a/x-pack/plugins/beats/server/routes/api/register_remove_tags_from_beats_route.js b/x-pack/plugins/beats/server/routes/api/register_remove_tags_from_beats_route.js deleted file mode 100644 index b5e66267b2ea4..0000000000000 --- a/x-pack/plugins/beats/server/routes/api/register_remove_tags_from_beats_route.js +++ /dev/null @@ -1,166 +0,0 @@ -/* - * 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 { - get, - flatten, - uniq -} from 'lodash'; -import { INDEX_NAMES } from '../../../common/constants'; -import { callWithRequestFactory } from '../../lib/client'; -import { wrapEsError } from '../../lib/error_wrappers'; - -async function getDocs(callWithRequest, ids) { - const params = { - index: INDEX_NAMES.BEATS, - type: '_doc', - body: { ids }, - _source: false - }; - - const response = await callWithRequest('mget', params); - return get(response, 'docs', []); -} - -function getBeats(callWithRequest, beatIds) { - const ids = beatIds.map(beatId => `beat:${beatId}`); - return getDocs(callWithRequest, ids); -} - -function getTags(callWithRequest, tags) { - const ids = tags.map(tag => `tag:${tag}`); - return getDocs(callWithRequest, ids); -} - -async function findNonExistentItems(callWithRequest, items, getFn) { - const itemsFromEs = await getFn.call(null, callWithRequest, items); - return itemsFromEs.reduce((nonExistentItems, itemFromEs, idx) => { - if (!itemFromEs.found) { - nonExistentItems.push(items[idx]); - } - return nonExistentItems; - }, []); -} - -function findNonExistentBeatIds(callWithRequest, beatIds) { - return findNonExistentItems(callWithRequest, beatIds, getBeats); -} - -function findNonExistentTags(callWithRequest, tags) { - return findNonExistentItems(callWithRequest, tags, getTags); -} - -async function persistRemovals(callWithRequest, removals) { - const body = flatten(removals.map(({ beatId, tag }) => { - const script = '' - + 'def beat = ctx._source.beat; ' - + 'if (beat.tags != null) { ' - + ' beat.tags.removeAll([params.tag]); ' - + '}'; - - return [ - { update: { _id: `beat:${beatId}` } }, - { script: { source: script, params: { tag } } } - ]; - })); - - const params = { - index: INDEX_NAMES.BEATS, - type: '_doc', - body, - refresh: 'wait_for' - }; - - const response = await callWithRequest('bulk', params); - return get(response, 'items', []) - .map((item, resultIdx) => ({ - status: item.update.status, - result: item.update.result, - idxInRequest: removals[resultIdx].idxInRequest - })); -} - -function addNonExistentItemRemovalsToResponse(response, removals, nonExistentBeatIds, nonExistentTags) { - removals.forEach(({ beat_id: beatId, tag }, idx) => { - const isBeatNonExistent = nonExistentBeatIds.includes(beatId); - const isTagNonExistent = nonExistentTags.includes(tag); - - if (isBeatNonExistent && isTagNonExistent) { - response.removals[idx].status = 404; - response.removals[idx].result = `Beat ${beatId} and tag ${tag} not found`; - } else if (isBeatNonExistent) { - response.removals[idx].status = 404; - response.removals[idx].result = `Beat ${beatId} not found`; - } else if (isTagNonExistent) { - response.removals[idx].status = 404; - response.removals[idx].result = `Tag ${tag} not found`; - } - }); -} - -function addRemovalResultsToResponse(response, removalResults) { - removalResults.forEach(removalResult => { - const { idxInRequest, status, result } = removalResult; - response.removals[idxInRequest].status = status; - response.removals[idxInRequest].result = result; - }); -} - -// TODO: add license check pre-hook -// TODO: write to Kibana audit log file -export function registerRemoveTagsFromBeatsRoute(server) { - server.route({ - method: 'POST', - path: '/api/beats/agents_tags/removals', - config: { - validate: { - payload: Joi.object({ - removals: Joi.array().items(Joi.object({ - beat_id: Joi.string().required(), - tag: Joi.string().required() - })) - }).required() - } - }, - handler: async (request, reply) => { - const callWithRequest = callWithRequestFactory(server, request); - - const { removals } = request.payload; - const beatIds = uniq(removals.map(removal => removal.beat_id)); - const tags = uniq(removals.map(removal => removal.tag)); - - const response = { - removals: removals.map(() => ({ status: null })) - }; - - try { - // Handle removals containing non-existing beat IDs or tags - const nonExistentBeatIds = await findNonExistentBeatIds(callWithRequest, beatIds); - const nonExistentTags = await findNonExistentTags(callWithRequest, tags); - - addNonExistentItemRemovalsToResponse(response, removals, nonExistentBeatIds, nonExistentTags); - - const validRemovals = removals - .map((removal, idxInRequest) => ({ - beatId: removal.beat_id, - tag: removal.tag, - idxInRequest // so we can add the result of this removal to the correct place in the response - })) - .filter((removal, idx) => response.removals[idx].status === null); - - if (validRemovals.length > 0) { - const removalResults = await persistRemovals(callWithRequest, validRemovals); - addRemovalResultsToResponse(response, removalResults); - } - } catch (err) { - return reply(wrapEsError(err)); - } - - reply(response); - } - }); -} diff --git a/x-pack/plugins/beats/server/routes/api/register_set_tag_route.js b/x-pack/plugins/beats/server/routes/api/register_set_tag_route.js deleted file mode 100644 index 288fcade9929b..0000000000000 --- a/x-pack/plugins/beats/server/routes/api/register_set_tag_route.js +++ /dev/null @@ -1,124 +0,0 @@ -/* - * 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 { - get, - uniq, - intersection -} from 'lodash'; -import { - INDEX_NAMES, - CONFIGURATION_BLOCKS -} from '../../../common/constants'; -import { callWithRequestFactory } from '../../lib/client'; -import { wrapEsError } from '../../lib/error_wrappers'; - -function validateUniquenessEnforcingTypes(configurationBlocks) { - const types = uniq(configurationBlocks.map(block => block.type)); - - // If none of the types in the given configuration blocks are uniqueness-enforcing, - // we don't need to perform any further validation checks. - const uniquenessEnforcingTypes = intersection(types, CONFIGURATION_BLOCKS.UNIQUENESS_ENFORCING_TYPES); - if (uniquenessEnforcingTypes.length === 0) { - return { isValid: true }; - } - - // Count the number of uniqueness-enforcing types in the given configuration blocks - const typeCountMap = configurationBlocks.reduce((typeCountMap, block) => { - const { type } = block; - if (!uniquenessEnforcingTypes.includes(type)) { - return typeCountMap; - } - - const count = typeCountMap[type] || 0; - return { - ...typeCountMap, - [type]: count + 1 - }; - }, {}); - - // If there is no more than one of any uniqueness-enforcing types in the given - // configuration blocks, we don't need to perform any further validation checks. - if (Object.values(typeCountMap).filter(count => count > 1).length === 0) { - return { isValid: true }; - } - - const message = Object.entries(typeCountMap) - .filter(([, count]) => count > 1) - .map(([type, count]) => `Expected only one configuration block of type '${type}' but found ${count}`) - .join(' '); - - return { - isValid: false, - message - }; -} - -async function validateConfigurationBlocks(configurationBlocks) { - return validateUniquenessEnforcingTypes(configurationBlocks); -} - -async function persistTag(callWithRequest, tag) { - const body = { - type: 'tag', - tag - }; - - const params = { - index: INDEX_NAMES.BEATS, - type: '_doc', - id: `tag:${tag.id}`, - body, - refresh: 'wait_for' - }; - - const response = await callWithRequest('index', params); - return response.result; -} - -// TODO: add license check pre-hook -// TODO: write to Kibana audit log file -export function registerSetTagRoute(server) { - server.route({ - method: 'PUT', - path: '/api/beats/tag/{tag}', - config: { - validate: { - payload: Joi.object({ - configuration_blocks: Joi.array().items( - Joi.object({ - type: Joi.string().required().valid(Object.values(CONFIGURATION_BLOCKS.TYPES)), - block_yml: Joi.string().required() - }) - ) - }).allow(null) - } - }, - handler: async (request, reply) => { - const callWithRequest = callWithRequestFactory(server, request); - - let result; - try { - const configurationBlocks = get(request, 'payload.configuration_blocks', []); - const { isValid, message } = await validateConfigurationBlocks(configurationBlocks); - if (!isValid) { - return reply({ message }).code(400); - } - - const tag = { - id: request.params.tag, - configuration_blocks: configurationBlocks - }; - result = await persistTag(callWithRequest, tag); - } catch (err) { - return reply(wrapEsError(err)); - } - - reply().code(result === 'created' ? 201 : 200); - } - }); -} diff --git a/x-pack/plugins/beats/server/routes/api/register_update_beat_route.js b/x-pack/plugins/beats/server/routes/api/register_update_beat_route.js deleted file mode 100644 index 5955e65f6bbaf..0000000000000 --- a/x-pack/plugins/beats/server/routes/api/register_update_beat_route.js +++ /dev/null @@ -1,101 +0,0 @@ -/* - * 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 { get } from 'lodash'; -import { INDEX_NAMES } from '../../../common/constants'; -import { callWithInternalUserFactory } from '../../lib/client'; -import { wrapEsError } from '../../lib/error_wrappers'; -import { areTokensEqual } from '../../lib/crypto'; - -async function getBeat(callWithInternalUser, beatId) { - const params = { - index: INDEX_NAMES.BEATS, - type: '_doc', - id: `beat:${beatId}`, - ignore: [ 404 ] - }; - - const response = await callWithInternalUser('get', params); - if (!response.found) { - return null; - } - - return get(response, '_source.beat'); -} - -function persistBeat(callWithInternalUser, beat) { - const body = { - type: 'beat', - beat - }; - - const params = { - index: INDEX_NAMES.BEATS, - type: '_doc', - id: `beat:${beat.id}`, - body, - refresh: 'wait_for' - }; - return callWithInternalUser('index', params); -} - -// TODO: add license check pre-hook -// TODO: write to Kibana audit log file (include who did the verification as well) -export function registerUpdateBeatRoute(server) { - server.route({ - method: 'PUT', - path: '/api/beats/agent/{beatId}', - config: { - validate: { - payload: Joi.object({ - type: Joi.string(), - version: Joi.string(), - host_name: Joi.string(), - ephemeral_id: Joi.string(), - local_configuration_yml: Joi.string(), - metadata: Joi.object() - }).required(), - headers: Joi.object({ - 'kbn-beats-access-token': Joi.string().required() - }).options({ allowUnknown: true }) - }, - auth: false - }, - handler: async (request, reply) => { - const callWithInternalUser = callWithInternalUserFactory(server); - const { beatId } = request.params; - - try { - const beat = await getBeat(callWithInternalUser, beatId); - if (beat === null) { - return reply({ message: 'Beat not found' }).code(404); - } - - const isAccessTokenValid = areTokensEqual(beat.access_token, request.headers['kbn-beats-access-token']); - if (!isAccessTokenValid) { - return reply({ message: 'Invalid access token' }).code(401); - } - - const isBeatVerified = beat.hasOwnProperty('verified_on'); - if (!isBeatVerified) { - return reply({ message: 'Beat has not been verified' }).code(400); - } - - const remoteAddress = request.info.remoteAddress; - await persistBeat(callWithInternalUser, { - ...beat, - ...request.payload, - host_ip: remoteAddress - }); - } catch (err) { - return reply(wrapEsError(err)); - } - - reply().code(204); - } - }); -} diff --git a/x-pack/plugins/beats/server/routes/api/register_verify_beats_route.js b/x-pack/plugins/beats/server/routes/api/register_verify_beats_route.js deleted file mode 100644 index b2113029224a5..0000000000000 --- a/x-pack/plugins/beats/server/routes/api/register_verify_beats_route.js +++ /dev/null @@ -1,143 +0,0 @@ -/* - * 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 moment from 'moment'; -import { - get, - flatten -} from 'lodash'; -import { INDEX_NAMES } from '../../../common/constants'; -import { callWithRequestFactory } from '../../lib/client'; -import { wrapEsError } from '../../lib/error_wrappers'; - -async function getBeats(callWithRequest, beatIds) { - const ids = beatIds.map(beatId => `beat:${beatId}`); - const params = { - index: INDEX_NAMES.BEATS, - type: '_doc', - body: { ids }, - _sourceInclude: [ 'beat.id', 'beat.verified_on' ] - }; - - const response = await callWithRequest('mget', params); - return get(response, 'docs', []); -} - -async function verifyBeats(callWithRequest, beatIds) { - if (!Array.isArray(beatIds) || (beatIds.length === 0)) { - return []; - } - - const verifiedOn = moment().toJSON(); - const body = flatten(beatIds.map(beatId => [ - { update: { _id: `beat:${beatId}` } }, - { doc: { beat: { verified_on: verifiedOn } } } - ])); - - const params = { - index: INDEX_NAMES.BEATS, - type: '_doc', - body, - refresh: 'wait_for' - }; - - const response = await callWithRequest('bulk', params); - return get(response, 'items', []); -} - -function findNonExistentBeatIds(beatsFromEs, beatIdsFromRequest) { - return beatsFromEs.reduce((nonExistentBeatIds, beatFromEs, idx) => { - if (!beatFromEs.found) { - nonExistentBeatIds.push(beatIdsFromRequest[idx]); - } - return nonExistentBeatIds; - }, []); -} - -function findAlreadyVerifiedBeatIds(beatsFromEs) { - return beatsFromEs - .filter(beat => beat.found) - .filter(beat => beat._source.beat.hasOwnProperty('verified_on')) - .map(beat => beat._source.beat.id); -} - -function findToBeVerifiedBeatIds(beatsFromEs) { - return beatsFromEs - .filter(beat => beat.found) - .filter(beat => !beat._source.beat.hasOwnProperty('verified_on')) - .map(beat => beat._source.beat.id); -} - -function findVerifiedBeatIds(verifications, toBeVerifiedBeatIds) { - return verifications.reduce((verifiedBeatIds, verification, idx) => { - if (verification.update.status === 200) { - verifiedBeatIds.push(toBeVerifiedBeatIds[idx]); - } - return verifiedBeatIds; - }, []); -} - -// TODO: add license check pre-hook -// TODO: write to Kibana audit log file -export function registerVerifyBeatsRoute(server) { - server.route({ - method: 'POST', - path: '/api/beats/agents/verify', - config: { - validate: { - payload: Joi.object({ - beats: Joi.array({ - id: Joi.string().required() - }).min(1) - }).required() - } - }, - handler: async (request, reply) => { - const callWithRequest = callWithRequestFactory(server, request); - - const beats = [...request.payload.beats]; - const beatIds = beats.map(beat => beat.id); - - let nonExistentBeatIds; - let alreadyVerifiedBeatIds; - let verifiedBeatIds; - - try { - const beatsFromEs = await getBeats(callWithRequest, beatIds); - - nonExistentBeatIds = findNonExistentBeatIds(beatsFromEs, beatIds); - alreadyVerifiedBeatIds = findAlreadyVerifiedBeatIds(beatsFromEs); - const toBeVerifiedBeatIds = findToBeVerifiedBeatIds(beatsFromEs); - - const verifications = await verifyBeats(callWithRequest, toBeVerifiedBeatIds); - verifiedBeatIds = findVerifiedBeatIds(verifications, toBeVerifiedBeatIds); - - } catch (err) { - return reply(wrapEsError(err)); - } - - beats.forEach(beat => { - if (nonExistentBeatIds.includes(beat.id)) { - beat.status = 404; - beat.result = 'not found'; - } else if (alreadyVerifiedBeatIds.includes(beat.id)) { - beat.status = 200; - beat.result = 'already verified'; - } else if (verifiedBeatIds.includes(beat.id)) { - beat.status = 200; - beat.result = 'verified'; - } else { - beat.status = 400; - beat.result = 'not verified'; - } - }); - - const response = { beats }; - reply(response); - } - }); -} diff --git a/x-pack/plugins/beats/server/utils/README.md b/x-pack/plugins/beats/server/utils/README.md new file mode 100644 index 0000000000000..8a6a27aa29867 --- /dev/null +++ b/x-pack/plugins/beats/server/utils/README.md @@ -0,0 +1 @@ +Utils should be data processing functions and other tools.... all in all utils is basicly everything that is not an adaptor, or presenter and yet too much to put in a lib. \ No newline at end of file diff --git a/x-pack/plugins/beats/server/lib/error_wrappers/index.js b/x-pack/plugins/beats/server/utils/error_wrappers/index.ts similarity index 100% rename from x-pack/plugins/beats/server/lib/error_wrappers/index.js rename to x-pack/plugins/beats/server/utils/error_wrappers/index.ts diff --git a/x-pack/plugins/beats/server/lib/error_wrappers/wrap_es_error.test.js b/x-pack/plugins/beats/server/utils/error_wrappers/wrap_es_error.test.js similarity index 90% rename from x-pack/plugins/beats/server/lib/error_wrappers/wrap_es_error.test.js rename to x-pack/plugins/beats/server/utils/error_wrappers/wrap_es_error.test.js index ec7338262844a..03b04a2ef61d2 100644 --- a/x-pack/plugins/beats/server/lib/error_wrappers/wrap_es_error.test.js +++ b/x-pack/plugins/beats/server/utils/error_wrappers/wrap_es_error.test.js @@ -8,7 +8,6 @@ import { wrapEsError } from './wrap_es_error'; describe('wrap_es_error', () => { describe('#wrapEsError', () => { - let originalError; beforeEach(() => { originalError = new Error('I am an error'); @@ -34,7 +33,9 @@ describe('wrap_es_error', () => { const wrappedError = wrapEsError(securityError); expect(wrappedError.isBoom).to.be(true); - expect(wrappedError.message).to.be('Insufficient user permissions for managing Logstash pipelines'); + expect(wrappedError.message).to.be( + 'Insufficient user permissions for managing Logstash pipelines' + ); }); }); }); diff --git a/x-pack/plugins/beats/server/lib/error_wrappers/wrap_es_error.js b/x-pack/plugins/beats/server/utils/error_wrappers/wrap_es_error.ts similarity index 79% rename from x-pack/plugins/beats/server/lib/error_wrappers/wrap_es_error.js rename to x-pack/plugins/beats/server/utils/error_wrappers/wrap_es_error.ts index d2abcab5c37dd..50ffbcb4a10c9 100644 --- a/x-pack/plugins/beats/server/lib/error_wrappers/wrap_es_error.js +++ b/x-pack/plugins/beats/server/utils/error_wrappers/wrap_es_error.ts @@ -13,10 +13,12 @@ import Boom from 'boom'; * @param err Object ES error * @return Object Boom error response */ -export function wrapEsError(err) { +export function wrapEsError(err: any) { const statusCode = err.statusCode; if (statusCode === 403) { - return Boom.forbidden('Insufficient user permissions for managing Beats configuration'); + return Boom.forbidden( + 'Insufficient user permissions for managing Beats configuration' + ); } return Boom.wrap(err, err.statusCode); } diff --git a/x-pack/plugins/beats/server/utils/find_non_existent_items.ts b/x-pack/plugins/beats/server/utils/find_non_existent_items.ts new file mode 100644 index 0000000000000..53e4066acc879 --- /dev/null +++ b/x-pack/plugins/beats/server/utils/find_non_existent_items.ts @@ -0,0 +1,14 @@ +/* + * 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 function findNonExistentItems(items: any, requestedItems: any) { + return items.reduce((nonExistentItems: any, item: any, idx: any) => { + if (!item.found) { + nonExistentItems.push(requestedItems[idx]); + } + return nonExistentItems; + }, []); +} diff --git a/x-pack/plugins/beats/server/lib/index_template/beats_template.json b/x-pack/plugins/beats/server/utils/index_templates/beats_template.json similarity index 97% rename from x-pack/plugins/beats/server/lib/index_template/beats_template.json rename to x-pack/plugins/beats/server/utils/index_templates/beats_template.json index 9f912f19b2a8d..0d00abbc5d759 100644 --- a/x-pack/plugins/beats/server/lib/index_template/beats_template.json +++ b/x-pack/plugins/beats/server/utils/index_templates/beats_template.json @@ -1,7 +1,5 @@ { - "index_patterns": [ - ".management-beats" - ], + "index_patterns": [".management-beats"], "version": 65000, "settings": { "index": { diff --git a/x-pack/plugins/beats/server/lib/index_template/index.js b/x-pack/plugins/beats/server/utils/index_templates/index.ts similarity index 73% rename from x-pack/plugins/beats/server/lib/index_template/index.js rename to x-pack/plugins/beats/server/utils/index_templates/index.ts index 04128e46ff0ea..eeaef7a68d49f 100644 --- a/x-pack/plugins/beats/server/lib/index_template/index.js +++ b/x-pack/plugins/beats/server/utils/index_templates/index.ts @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { installIndexTemplate } from './install_index_template'; +import beatsIndexTemplate from './beats_template.json'; +export { beatsIndexTemplate }; diff --git a/x-pack/plugins/beats/server/utils/polyfills.ts b/x-pack/plugins/beats/server/utils/polyfills.ts new file mode 100644 index 0000000000000..5291e2c72be7d --- /dev/null +++ b/x-pack/plugins/beats/server/utils/polyfills.ts @@ -0,0 +1,17 @@ +/* + * 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 entries = (obj: any) => { + const ownProps = Object.keys(obj); + let i = ownProps.length; + const resArray = new Array(i); // preallocate the Array + + while (i--) { + resArray[i] = [ownProps[i], obj[ownProps[i]]]; + } + + return resArray; +}; diff --git a/x-pack/plugins/beats/server/utils/wrap_request.ts b/x-pack/plugins/beats/server/utils/wrap_request.ts new file mode 100644 index 0000000000000..a29f9055f3688 --- /dev/null +++ b/x-pack/plugins/beats/server/utils/wrap_request.ts @@ -0,0 +1,24 @@ +/* + * 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 { FrameworkRequest, WrappableRequest } from '../lib/lib'; + +export const internalFrameworkRequest = Symbol('internalFrameworkRequest'); + +export function wrapRequest( + req: InternalRequest +): FrameworkRequest { + const { params, payload, query, headers, info } = req; + + return { + [internalFrameworkRequest]: req, + headers, + info, + params, + payload, + query, + }; +} diff --git a/x-pack/plugins/beats/tsconfig.json b/x-pack/plugins/beats/tsconfig.json new file mode 100644 index 0000000000000..4082f16a5d91c --- /dev/null +++ b/x-pack/plugins/beats/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.json" +} diff --git a/x-pack/plugins/beats/server/lib/crypto/index.js b/x-pack/plugins/beats/types/json.t.ts similarity index 77% rename from x-pack/plugins/beats/server/lib/crypto/index.js rename to x-pack/plugins/beats/types/json.t.ts index 31fa5de67b2ca..46af99f7f740b 100644 --- a/x-pack/plugins/beats/server/lib/crypto/index.js +++ b/x-pack/plugins/beats/types/json.t.ts @@ -4,4 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export { areTokensEqual } from './are_tokens_equal'; +declare module '*.json' { + const value: any; + export default value; +} diff --git a/x-pack/plugins/beats/wallaby.js b/x-pack/plugins/beats/wallaby.js new file mode 100644 index 0000000000000..c20488d35cfb6 --- /dev/null +++ b/x-pack/plugins/beats/wallaby.js @@ -0,0 +1,27 @@ +/* + * 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. + */ + +module.exports = function (wallaby) { + return { + debug: true, + files: [ + '../../tsconfig.json', + //'plugins/beats/public/**/*.+(js|jsx|ts|tsx|json|snap|css|less|sass|scss|jpg|jpeg|gif|png|svg)', + 'server/**/*.+(js|jsx|ts|tsx|json|snap|css|less|sass|scss|jpg|jpeg|gif|png|svg)', + 'common/**/*.+(js|jsx|ts|tsx|json|snap|css|less|sass|scss|jpg|jpeg|gif|png|svg)', + ], + + tests: ['**/*.test.ts'], + env: { + type: 'node', + runner: 'node', + }, + testFramework: 'jest', + compilers: { + '**/*.ts?(x)': wallaby.compilers.typeScript({ module: 'commonjs' }), + }, + }; +}; diff --git a/x-pack/yarn.lock b/x-pack/yarn.lock index 048fd159feb8f..c7a1d0ec50830 100644 --- a/x-pack/yarn.lock +++ b/x-pack/yarn.lock @@ -119,6 +119,10 @@ url-join "^4.0.0" ws "^4.1.0" +"@types/boom@^4.3.8": + version "4.3.10" + resolved "https://registry.yarnpkg.com/@types/boom/-/boom-4.3.10.tgz#39dad8c0614c26b91ef016a57d7eee4ffe4f8a25" + "@types/delay@^2.0.1": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/delay/-/delay-2.0.1.tgz#61bcf318a74b61e79d1658fbf054f984c90ef901" @@ -139,6 +143,12 @@ dependencies: "@types/node" "*" +"@types/hapi@15.0.1": + version "15.0.1" + resolved "https://registry.yarnpkg.com/@types/hapi/-/hapi-15.0.1.tgz#919e1d3a9160a080c9fdefaccc892239772e1258" + dependencies: + "@types/node" "*" + "@types/is-stream@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@types/is-stream/-/is-stream-1.1.0.tgz#b84d7bb207a210f2af9bed431dc0fbe9c4143be1" @@ -149,6 +159,14 @@ version "22.2.3" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-22.2.3.tgz#0157c0316dc3722c43a7b71de3fdf3acbccef10d" +"@types/joi@^10.4.0": + version "10.6.2" + resolved "https://registry.yarnpkg.com/@types/joi/-/joi-10.6.2.tgz#0e7d632fe918c337784e87b16c7cc0098876179a" + +"@types/lodash@^3.10.0": + version "3.10.2" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-3.10.2.tgz#c1fbda1562ef5603c8192fe1fe65b017849d5873" + "@types/loglevel@^1.5.3": version "1.5.3" resolved "https://registry.yarnpkg.com/@types/loglevel/-/loglevel-1.5.3.tgz#adfce55383edc5998a2170ad581b3e23d6adb5b8" @@ -175,9 +193,9 @@ dependencies: "@types/retry" "*" -"@types/pngjs@^3.3.1": - version "3.3.1" - resolved "https://registry.yarnpkg.com/@types/pngjs/-/pngjs-3.3.1.tgz#47d97bd29dd6372856050e9e5e366517dd1ba2d8" +"@types/pngjs@^3.3.0": + version "3.3.2" + resolved "https://registry.yarnpkg.com/@types/pngjs/-/pngjs-3.3.2.tgz#8ed3bd655ab3a92ea32ada7a21f618e63b93b1d4" dependencies: "@types/node" "*" @@ -189,6 +207,12 @@ version "0.8.2" resolved "https://registry.yarnpkg.com/@types/url-join/-/url-join-0.8.2.tgz#1181ecbe1d97b7034e0ea1e35e62e86cc26b422d" +"@types/uuid@^3.4.3": + version "3.4.3" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.3.tgz#121ace265f5569ce40f4f6d0ff78a338c732a754" + dependencies: + "@types/node" "*" + "@types/ws@^4.0.1": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-4.0.2.tgz#b29037627dd7ba31ec49a4f1584840422efb856f" diff --git a/yarn.lock b/yarn.lock index 20b4cc6d412f0..92bd3108bf025 100644 --- a/yarn.lock +++ b/yarn.lock @@ -543,6 +543,12 @@ version "0.8.2" resolved "https://registry.yarnpkg.com/@types/url-join/-/url-join-0.8.2.tgz#1181ecbe1d97b7034e0ea1e35e62e86cc26b422d" +"@types/uuid@^3.4.3": + version "3.4.3" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.3.tgz#121ace265f5569ce40f4f6d0ff78a338c732a754" + dependencies: + "@types/node" "*" + "@types/ws@^4.0.1": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-4.0.2.tgz#b29037627dd7ba31ec49a4f1584840422efb856f" @@ -12219,7 +12225,7 @@ source-map-support@^0.5.0: dependencies: source-map "^0.6.0" -source-map-support@^0.5.3, source-map-support@^0.5.5: +source-map-support@^0.5.5, source-map-support@^0.5.6: version "0.5.6" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.6.tgz#4435cee46b1aab62b8e8610ce60f788091c51c13" dependencies: @@ -13188,17 +13194,16 @@ ts-loader@^3.5.0: micromatch "^3.1.4" semver "^5.0.1" -ts-node@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-6.0.3.tgz#28bf74bcad134fad17f7469dad04638ece03f0f4" +ts-node@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-6.1.1.tgz#19607140acb06150441fcdb61be11f73f7b6657e" dependencies: arrify "^1.0.0" - chalk "^2.3.0" diff "^3.1.0" make-error "^1.1.1" minimist "^1.2.0" mkdirp "^0.5.1" - source-map-support "^0.5.3" + source-map-support "^0.5.6" yn "^2.0.0" tslib@^1.7.1, tslib@^1.8.0, tslib@^1.8.1: From c4d0776ff75bff3755676ef7371fe50ede848479 Mon Sep 17 00:00:00 2001 From: Matt Apperson Date: Fri, 6 Jul 2018 18:54:27 -0400 Subject: [PATCH 20/43] [Beats Management] Move tokens to use JWT, add more complete test suite (#20317) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * inital effort to move to JWT and added jest based tests on libs * assign beats tests all passing * token tests now pass * add more tests * all tests now green * fix broken test, this is beats CM not logstash 😊 * added readme * move enrollment token back to a hash * remove un-needed comment * alias lodash get to avoid confusion * isolated hash creation --- x-pack/package.json | 6 + x-pack/plugins/beats/index.ts | 1 + x-pack/plugins/beats/readme.md | 7 + .../beats/elasticsearch_beats_adapter.ts | 32 +-- .../adapters/beats/memory_beats_adapter.ts | 123 ++++++++++ .../kibana/kibana_framework_adapter.ts | 26 +- .../kibana/testing_framework_adapter.ts | 83 +++++++ .../tags/elasticsearch_tags_adapter.ts | 10 +- .../lib/adapters/tags/memory_tags_adapter.ts | 29 +++ .../tokens/elasticsearch_tokens_adapter.ts | 1 + .../adapters/tokens/memory_tokens_adapter.ts | 47 ++++ .../__tests__/beats/assign_tags.test.ts | 231 ++++++++++++++++++ .../domains/__tests__/beats/enroll.test.ts | 136 +++++++++++ .../domains/__tests__/beats/verify.test.ts | 192 +++++++++++++++ .../lib/domains/__tests__/tokens.test.ts | 87 +++++++ .../plugins/beats/server/lib/domains/beats.ts | 41 ++-- .../beats/server/lib/domains/tokens.ts | 94 ++++++- x-pack/plugins/beats/server/lib/lib.ts | 22 +- .../beats/server/rest_api/beats/enroll.ts | 13 +- .../beats/server/rest_api/beats/update.ts | 2 +- .../beats/server/rest_api/beats/verify.ts | 13 +- .../error_wrappers/wrap_es_error.test.js | 3 +- .../server/utils/find_non_existent_items.ts | 26 +- x-pack/plugins/beats/wallaby.js | 39 ++- .../api_integration/apis/beats/enroll_beat.js | 83 +++---- .../api_integration/apis/beats/update_beat.js | 75 +++--- .../apis/beats/verify_beats.js | 29 +-- .../es_archives/beats/list/data.json.gz | Bin 447 -> 521 bytes 28 files changed, 1254 insertions(+), 197 deletions(-) create mode 100644 x-pack/plugins/beats/readme.md create mode 100644 x-pack/plugins/beats/server/lib/adapters/beats/memory_beats_adapter.ts create mode 100644 x-pack/plugins/beats/server/lib/adapters/famework/kibana/testing_framework_adapter.ts create mode 100644 x-pack/plugins/beats/server/lib/adapters/tags/memory_tags_adapter.ts create mode 100644 x-pack/plugins/beats/server/lib/adapters/tokens/memory_tokens_adapter.ts create mode 100644 x-pack/plugins/beats/server/lib/domains/__tests__/beats/assign_tags.test.ts create mode 100644 x-pack/plugins/beats/server/lib/domains/__tests__/beats/enroll.test.ts create mode 100644 x-pack/plugins/beats/server/lib/domains/__tests__/beats/verify.test.ts create mode 100644 x-pack/plugins/beats/server/lib/domains/__tests__/tokens.test.ts diff --git a/x-pack/package.json b/x-pack/package.json index 555aed114a2c2..98267d8e33bb1 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -28,11 +28,14 @@ "@kbn/plugin-helpers": "link:../packages/kbn-plugin-helpers", "@kbn/test": "link:../packages/kbn-test", "@types/boom": "^4.3.8", + "@types/chance": "^1.0.1", + "@types/expect.js": "^0.3.29", "@types/hapi": "15.0.1", "@types/jest": "^22.2.3", "@types/joi": "^10.4.0", "@types/lodash": "^3.10.0", "@types/pngjs": "^3.3.0", + "@types/sinon": "^5.0.1", "abab": "^1.0.4", "ansicolors": "0.3.2", "aws-sdk": "2.2.33", @@ -91,6 +94,8 @@ "@kbn/ui-framework": "link:../packages/kbn-ui-framework", "@samverschueren/stream-to-observable": "^0.3.0", "@slack/client": "^4.2.2", + "@types/elasticsearch": "^5.0.24", + "@types/jsonwebtoken": "^7.2.7", "@types/uuid": "^3.4.3", "angular-paging": "2.2.1", "angular-resource": "1.4.9", @@ -123,6 +128,7 @@ "isomorphic-fetch": "2.2.1", "joi": "6.10.1", "jquery": "^3.3.1", + "jsonwebtoken": "^8.3.0", "jstimezonedetect": "1.0.5", "lodash": "3.10.1", "lodash.mean": "^4.1.0", diff --git a/x-pack/plugins/beats/index.ts b/x-pack/plugins/beats/index.ts index ce9b8147dbe4b..ced89c186f73e 100644 --- a/x-pack/plugins/beats/index.ts +++ b/x-pack/plugins/beats/index.ts @@ -15,6 +15,7 @@ export function beats(kibana: any) { config: () => Joi.object({ enabled: Joi.boolean().default(true), + encryptionKey: Joi.string(), enrollmentTokensTtlInSeconds: Joi.number() .integer() .min(1) diff --git a/x-pack/plugins/beats/readme.md b/x-pack/plugins/beats/readme.md new file mode 100644 index 0000000000000..fdd56a393e573 --- /dev/null +++ b/x-pack/plugins/beats/readme.md @@ -0,0 +1,7 @@ +# Documentation for Beats CM in x-pack kibana + +### Run tests + +``` +node scripts/jest.js plugins/beats --watch +``` diff --git a/x-pack/plugins/beats/server/lib/adapters/beats/elasticsearch_beats_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/beats/elasticsearch_beats_adapter.ts index 283f65c1258ae..76fbf956dafc9 100644 --- a/x-pack/plugins/beats/server/lib/adapters/beats/elasticsearch_beats_adapter.ts +++ b/x-pack/plugins/beats/server/lib/adapters/beats/elasticsearch_beats_adapter.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { flatten, get, omit } from 'lodash'; +import { flatten, get as _get, omit } from 'lodash'; import moment from 'moment'; import { INDEX_NAMES } from '../../../../common/constants'; import { @@ -35,7 +35,7 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { return null; } - return get(response, '_source.beat'); + return _get(response, '_source.beat'); } public async insert(beat: CMBeat) { @@ -73,22 +73,6 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { public async getWithIds(req: FrameworkRequest, beatIds: string[]) { const ids = beatIds.map(beatId => `beat:${beatId}`); - const params = { - _source: false, - body: { - ids, - }, - index: INDEX_NAMES.BEATS, - type: '_doc', - }; - const response = await this.framework.callWithRequest(req, 'mget', params); - return get(response, 'docs', []); - } - - // TODO merge with getBeatsWithIds - public async getVerifiedWithIds(req: FrameworkRequest, beatIds: string[]) { - const ids = beatIds.map(beatId => `beat:${beatId}`); - const params = { _sourceInclude: ['beat.id', 'beat.verified_on'], body: { @@ -98,7 +82,10 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { type: '_doc', }; const response = await this.framework.callWithRequest(req, 'mget', params); - return get(response, 'docs', []); + + return get(response, 'docs', []) + .filter((b: any) => b.found) + .map((b: any) => b._source.beat); } public async verifyBeats(req: FrameworkRequest, beatIds: string[]) { @@ -115,6 +102,7 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { ); const params = { + _sourceInclude: ['beat.id', 'beat.verified_on'], body, index: INDEX_NAMES.BEATS, refresh: 'wait_for', @@ -122,7 +110,11 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { }; const response = await this.framework.callWithRequest(req, 'bulk', params); - return get(response, 'items', []); + + return _get(response, 'items', []).map(b => ({ + ..._get(b, 'update.get._source.beat', {}), + updateStatus: _get(b, 'update.result', 'unknown error'), + })); } public async getAll(req: FrameworkRequest) { diff --git a/x-pack/plugins/beats/server/lib/adapters/beats/memory_beats_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/beats/memory_beats_adapter.ts new file mode 100644 index 0000000000000..9de8297c0f73e --- /dev/null +++ b/x-pack/plugins/beats/server/lib/adapters/beats/memory_beats_adapter.ts @@ -0,0 +1,123 @@ +/* + * 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 { omit } from 'lodash'; +import moment from 'moment'; + +import { + CMBeat, + CMBeatsAdapter, + CMTagAssignment, + FrameworkRequest, +} from '../../lib'; + +export class MemoryBeatsAdapter implements CMBeatsAdapter { + private beatsDB: CMBeat[]; + + constructor(beatsDB: CMBeat[]) { + this.beatsDB = beatsDB; + } + + public async get(id: string) { + return this.beatsDB.find(beat => beat.id === id); + } + + public async insert(beat: CMBeat) { + this.beatsDB.push(beat); + } + + public async update(beat: CMBeat) { + const beatIndex = this.beatsDB.findIndex(b => b.id === beat.id); + + this.beatsDB[beatIndex] = { + ...this.beatsDB[beatIndex], + ...beat, + }; + } + + public async getWithIds(req: FrameworkRequest, beatIds: string[]) { + return this.beatsDB.filter(beat => beatIds.includes(beat.id)); + } + + public async verifyBeats(req: FrameworkRequest, beatIds: string[]) { + if (!Array.isArray(beatIds) || beatIds.length === 0) { + return []; + } + + const verifiedOn = moment().toJSON(); + + this.beatsDB.forEach((beat, i) => { + if (beatIds.includes(beat.id)) { + this.beatsDB[i].verified_on = verifiedOn; + } + }); + + return this.beatsDB.filter(beat => beatIds.includes(beat.id)); + } + + public async getAll(req: FrameworkRequest) { + return this.beatsDB.map((beat: any) => omit(beat, ['access_token'])); + } + + public async removeTagsFromBeats( + req: FrameworkRequest, + removals: CMTagAssignment[] + ): Promise { + const beatIds = removals.map(r => r.beatId); + + const response = this.beatsDB + .filter(beat => beatIds.includes(beat.id)) + .map(beat => { + const tagData = removals.find(r => r.beatId === beat.id); + if (tagData) { + if (beat.tags) { + beat.tags = beat.tags.filter(tag => tag !== tagData.tag); + } + } + return beat; + }); + + return response.map((item: CMBeat, resultIdx: number) => ({ + idxInRequest: removals[resultIdx].idxInRequest, + result: 'updated', + status: 200, + })); + } + + public async assignTagsToBeats( + req: FrameworkRequest, + assignments: CMTagAssignment[] + ): Promise { + const beatIds = assignments.map(r => r.beatId); + + this.beatsDB.filter(beat => beatIds.includes(beat.id)).map(beat => { + // get tags that need to be assigned to this beat + const tags = assignments + .filter(a => a.beatId === beat.id) + .map((t: CMTagAssignment) => t.tag); + + if (tags.length > 0) { + if (!beat.tags) { + beat.tags = []; + } + const nonExistingTags = tags.filter( + (t: string) => beat.tags && !beat.tags.includes(t) + ); + + if (nonExistingTags.length > 0) { + beat.tags = beat.tags.concat(nonExistingTags); + } + } + return beat; + }); + + return assignments.map((item: CMTagAssignment, resultIdx: number) => ({ + idxInRequest: assignments[resultIdx].idxInRequest, + result: 'updated', + status: 200, + })); + } +} diff --git a/x-pack/plugins/beats/server/lib/adapters/famework/kibana/kibana_framework_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/famework/kibana/kibana_framework_adapter.ts index 6fc2fc4853b03..a54997370ac5d 100644 --- a/x-pack/plugins/beats/server/lib/adapters/famework/kibana/kibana_framework_adapter.ts +++ b/x-pack/plugins/beats/server/lib/adapters/famework/kibana/kibana_framework_adapter.ts @@ -19,18 +19,25 @@ import { export class KibanaBackendFrameworkAdapter implements BackendFrameworkAdapter { public version: string; - private server: Server; + private cryptoHash: string | null; constructor(hapiServer: Server) { this.server = hapiServer; this.version = hapiServer.plugins.kibana.status.plugin.version; + this.cryptoHash = null; + + this.validateConfig(); } public getSetting(settingPath: string) { - // TODO type check this properly + // TODO type check server properly + if (settingPath === 'xpack.beats.encryptionKey') { + // @ts-ignore + return this.server.config().get(settingPath) || this.cryptoHash; + } // @ts-ignore - return this.server.config().get(settingPath); + return this.server.config().get(settingPath) || this.cryptoHash; } public exposeStaticDir(urlPath: string, dir: string): void { @@ -79,4 +86,17 @@ export class KibanaBackendFrameworkAdapter implements BackendFrameworkAdapter { const fields = await callWithRequest(internalRequest, ...rest); return fields; } + + private validateConfig() { + // @ts-ignore + const config = this.server.config(); + const encryptionKey = config.get('xpack.beats.encryptionKey'); + + if (!encryptionKey) { + this.server.log( + 'Using a default encryption key for xpack.beats.encryptionKey. It is recommended that you set xpack.beats.encryptionKey in kibana.yml with a unique token' + ); + this.cryptoHash = 'xpack_beats_default_encryptionKey'; + } + } } diff --git a/x-pack/plugins/beats/server/lib/adapters/famework/kibana/testing_framework_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/famework/kibana/testing_framework_adapter.ts new file mode 100644 index 0000000000000..9c928a05cfd5a --- /dev/null +++ b/x-pack/plugins/beats/server/lib/adapters/famework/kibana/testing_framework_adapter.ts @@ -0,0 +1,83 @@ +/* + * 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 { Client } from 'elasticsearch'; +import { Request } from 'hapi'; +import { get } from 'lodash'; +import { + BackendFrameworkAdapter, + FrameworkRequest, + FrameworkRouteOptions, + WrappableRequest, +} from '../../../lib'; + +interface TestSettings { + enrollmentTokensTtlInSeconds: number; + encryptionKey: string; +} + +export class TestingBackendFrameworkAdapter implements BackendFrameworkAdapter { + public version: string; + private client: Client | null; + private settings: TestSettings; + + constructor(client: Client | null, settings: TestSettings) { + this.client = client; + this.settings = settings || { + encryptionKey: 'something_who_cares', + enrollmentTokensTtlInSeconds: 10 * 60, // 10 minutes + }; + this.version = 'testing'; + } + + public getSetting(settingPath: string) { + switch (settingPath) { + case 'xpack.beats.enrollmentTokensTtlInSeconds': + return this.settings.enrollmentTokensTtlInSeconds; + case 'xpack.beats.encryptionKey': + return this.settings.encryptionKey; + } + } + + public exposeStaticDir(urlPath: string, dir: string): void { + // not yet testable + } + + public registerRoute( + route: FrameworkRouteOptions + ) { + // not yet testable + } + + public installIndexTemplate(name: string, template: {}) { + if (this.client) { + return this.client.indices.putTemplate({ + body: template, + name, + }); + } + } + + public async callWithInternalUser(esMethod: string, options: {}) { + const api = get(this.client, esMethod); + + api(options); + + return await api(options); + } + + public async callWithRequest( + req: FrameworkRequest, + esMethod: string, + options: {} + ) { + const api = get(this.client, esMethod); + + api(options); + + return await api(options); + } +} diff --git a/x-pack/plugins/beats/server/lib/adapters/tags/elasticsearch_tags_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/tags/elasticsearch_tags_adapter.ts index 2293ba77677fd..44aea344151ca 100644 --- a/x-pack/plugins/beats/server/lib/adapters/tags/elasticsearch_tags_adapter.ts +++ b/x-pack/plugins/beats/server/lib/adapters/tags/elasticsearch_tags_adapter.ts @@ -25,7 +25,7 @@ export class ElasticsearchTagsAdapter implements CMTagsAdapter { // TODO abstract to kibana adapter as the more generic getDocs const params = { - _source: false, + _sourceInclude: ['tag.configuration_blocks'], body: { ids, }, @@ -33,7 +33,13 @@ export class ElasticsearchTagsAdapter implements CMTagsAdapter { type: '_doc', }; const response = await this.framework.callWithRequest(req, 'mget', params); - return get(response, 'docs', []); + + return get(response, 'docs', []) + .filter((b: any) => b.found) + .map((b: any) => ({ + ...b._source.tag, + id: b._id.replace('tag:', ''), + })); } public async upsertTag(req: FrameworkRequest, tag: BeatTag) { diff --git a/x-pack/plugins/beats/server/lib/adapters/tags/memory_tags_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/tags/memory_tags_adapter.ts new file mode 100644 index 0000000000000..4d2a80e1b39c2 --- /dev/null +++ b/x-pack/plugins/beats/server/lib/adapters/tags/memory_tags_adapter.ts @@ -0,0 +1,29 @@ +/* + * 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 { BeatTag, CMTagsAdapter, FrameworkRequest } from '../../lib'; + +export class MemoryTagsAdapter implements CMTagsAdapter { + private tagsDB: BeatTag[] = []; + + constructor(tagsDB: BeatTag[]) { + this.tagsDB = tagsDB; + } + + public async getTagsWithIds(req: FrameworkRequest, tagIds: string[]) { + return this.tagsDB.filter(tag => tagIds.includes(tag.id)); + } + + public async upsertTag(req: FrameworkRequest, tag: BeatTag) { + const existingTagIndex = this.tagsDB.findIndex(t => t.id === tag.id); + if (existingTagIndex !== -1) { + this.tagsDB[existingTagIndex] = tag; + } else { + this.tagsDB.push(tag); + } + return tag; + } +} diff --git a/x-pack/plugins/beats/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts index c8969c7ab08d0..7a63c784ecf6a 100644 --- a/x-pack/plugins/beats/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts +++ b/x-pack/plugins/beats/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts @@ -79,5 +79,6 @@ export class ElasticsearchTokensAdapter implements CMTokensAdapter { }; await this.framework.callWithRequest(req, 'bulk', params); + return tokens; } } diff --git a/x-pack/plugins/beats/server/lib/adapters/tokens/memory_tokens_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/tokens/memory_tokens_adapter.ts new file mode 100644 index 0000000000000..1734327007e08 --- /dev/null +++ b/x-pack/plugins/beats/server/lib/adapters/tokens/memory_tokens_adapter.ts @@ -0,0 +1,47 @@ +/* + * 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 { CMTokensAdapter, EnrollmentToken, FrameworkRequest } from '../../lib'; + +export class MemoryTokensAdapter implements CMTokensAdapter { + private tokenDB: EnrollmentToken[]; + + constructor(tokenDB: EnrollmentToken[]) { + this.tokenDB = tokenDB; + } + + public async deleteEnrollmentToken(enrollmentToken: string) { + const index = this.tokenDB.findIndex( + token => token.token === enrollmentToken + ); + + if (index > -1) { + this.tokenDB.splice(index, 1); + } + } + + public async getEnrollmentToken( + tokenString: string + ): Promise { + return new Promise(resolve => { + return resolve(this.tokenDB.find(token => token.token === tokenString)); + }); + } + + public async upsertTokens(req: FrameworkRequest, tokens: EnrollmentToken[]) { + tokens.forEach(token => { + const existingIndex = this.tokenDB.findIndex( + t => t.token === token.token + ); + if (existingIndex !== -1) { + this.tokenDB[existingIndex] = token; + } else { + this.tokenDB.push(token); + } + }); + return tokens; + } +} diff --git a/x-pack/plugins/beats/server/lib/domains/__tests__/beats/assign_tags.test.ts b/x-pack/plugins/beats/server/lib/domains/__tests__/beats/assign_tags.test.ts new file mode 100644 index 0000000000000..c1e360ffd75f4 --- /dev/null +++ b/x-pack/plugins/beats/server/lib/domains/__tests__/beats/assign_tags.test.ts @@ -0,0 +1,231 @@ +/* + * 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 { wrapRequest } from '../../../../utils/wrap_request'; +import { MemoryBeatsAdapter } from '../../../adapters/beats/memory_beats_adapter'; +import { TestingBackendFrameworkAdapter } from '../../../adapters/famework/kibana/testing_framework_adapter'; +import { MemoryTagsAdapter } from '../../../adapters/tags/memory_tags_adapter'; +import { MemoryTokensAdapter } from '../../../adapters/tokens/memory_tokens_adapter'; + +import { BeatTag, CMBeat } from './../../../lib'; + +import { CMBeatsDomain } from '../../beats'; +import { CMTagsDomain } from '../../tags'; +import { CMTokensDomain } from '../../tokens'; + +import Chance from 'chance'; + +const seed = Date.now(); +const chance = new Chance(seed); + +const fakeReq = wrapRequest({ + headers: {}, + info: {}, + params: {}, + payload: {}, + query: {}, +}); + +const settings = { + encryptionKey: 'something_who_cares', + enrollmentTokensTtlInSeconds: 10 * 60, // 10 minutes +}; + +describe('Beats Domain Lib', () => { + let beatsLib: CMBeatsDomain; + let beatsDB: CMBeat[] = []; + let tagsDB: BeatTag[] = []; + + describe('assign_tags_to_beats', () => { + beforeEach(async () => { + beatsDB = [ + { + access_token: '9a6c99ae0fd84b068819701169cd8a4b', + host_ip: '1.2.3.4', + host_name: 'foo.bar.com', + id: 'qux', + type: 'filebeat', + }, + { + access_token: '188255eb560a4448b72656c5e99cae6f', + host_ip: '22.33.11.44', + host_name: 'baz.bar.com', + id: 'baz', + type: 'metricbeat', + }, + { + access_token: '93c4a4dd08564c189a7ec4e4f046b975', + host_ip: '1.2.3.4', + host_name: 'foo.bar.com', + id: 'foo', + tags: ['production', 'qa'], + type: 'metricbeat', + verified_on: '2018-05-15T16:25:38.924Z', + }, + { + access_token: '3c4a4dd08564c189a7ec4e4f046b9759', + host_ip: '11.22.33.44', + host_name: 'foo.com', + id: 'bar', + type: 'filebeat', + }, + ]; + tagsDB = [ + { + configuration_blocks: [], + id: 'production', + }, + { + configuration_blocks: [], + id: 'development', + }, + { + configuration_blocks: [], + id: 'qa', + }, + ]; + const framework = new TestingBackendFrameworkAdapter(null, settings); + + const tokensLib = new CMTokensDomain(new MemoryTokensAdapter([]), { + framework, + }); + + const tagsLib = new CMTagsDomain(new MemoryTagsAdapter(tagsDB)); + + beatsLib = new CMBeatsDomain(new MemoryBeatsAdapter(beatsDB), { + tags: tagsLib, + tokens: tokensLib, + }); + }); + + it('should add a single tag to a single beat', async () => { + const apiResponse = await beatsLib.assignTagsToBeats(fakeReq, [ + { beatId: 'bar', tag: 'production' }, + ]); + + expect(apiResponse.assignments).to.eql([ + { status: 200, result: 'updated' }, + ]); + }); + + it('should not re-add an existing tag to a beat', async () => { + const tags = ['production']; + + let beat = beatsDB.find(b => b.id === 'foo') as any; + expect(beat.tags).to.eql([...tags, 'qa']); + + // Adding the existing tag + const apiResponse = await beatsLib.assignTagsToBeats(fakeReq, [ + { beatId: 'foo', tag: 'production' }, + ]); + + expect(apiResponse.assignments).to.eql([ + { status: 200, result: 'updated' }, + ]); + + beat = beatsDB.find(b => b.id === 'foo') as any; + expect(beat.tags).to.eql([...tags, 'qa']); + }); + + it('should add a single tag to a multiple beats', async () => { + const apiResponse = await beatsLib.assignTagsToBeats(fakeReq, [ + { beatId: 'foo', tag: 'development' }, + { beatId: 'bar', tag: 'development' }, + ]); + + expect(apiResponse.assignments).to.eql([ + { status: 200, result: 'updated' }, + { status: 200, result: 'updated' }, + ]); + + let beat = beatsDB.find(b => b.id === 'foo') as any; + expect(beat.tags).to.eql(['production', 'qa', 'development']); // as beat 'foo' already had 'production' and 'qa' tags attached to it + + beat = beatsDB.find(b => b.id === 'bar') as any; + expect(beat.tags).to.eql(['development']); + }); + + it('should add multiple tags to a single beat', async () => { + const apiResponse = await beatsLib.assignTagsToBeats(fakeReq, [ + { beatId: 'bar', tag: 'development' }, + { beatId: 'bar', tag: 'production' }, + ]); + + expect(apiResponse.assignments).to.eql([ + { status: 200, result: 'updated' }, + { status: 200, result: 'updated' }, + ]); + + const beat = beatsDB.find(b => b.id === 'bar') as any; + expect(beat.tags).to.eql(['development', 'production']); + }); + + it('should add multiple tags to a multiple beats', async () => { + const apiResponse = await beatsLib.assignTagsToBeats(fakeReq, [ + { beatId: 'foo', tag: 'development' }, + { beatId: 'bar', tag: 'production' }, + ]); + + expect(apiResponse.assignments).to.eql([ + { status: 200, result: 'updated' }, + { status: 200, result: 'updated' }, + ]); + + let beat = beatsDB.find(b => b.id === 'foo') as any; + expect(beat.tags).to.eql(['production', 'qa', 'development']); // as beat 'foo' already had 'production' and 'qa' tags attached to it + + beat = beatsDB.find(b => b.id === 'bar') as any; + expect(beat.tags).to.eql(['production']); + }); + + it('should return errors for non-existent beats', async () => { + const nonExistentBeatId = chance.word(); + + const apiResponse = await beatsLib.assignTagsToBeats(fakeReq, [ + { beatId: nonExistentBeatId, tag: 'production' }, + ]); + + expect(apiResponse.assignments).to.eql([ + { status: 404, result: `Beat ${nonExistentBeatId} not found` }, + ]); + }); + + it('should return errors for non-existent tags', async () => { + const nonExistentTag = chance.word(); + + const apiResponse = await beatsLib.assignTagsToBeats(fakeReq, [ + { beatId: 'bar', tag: nonExistentTag }, + ]); + + expect(apiResponse.assignments).to.eql([ + { status: 404, result: `Tag ${nonExistentTag} not found` }, + ]); + + const beat = beatsDB.find(b => b.id === 'bar') as any; + expect(beat).to.not.have.property('tags'); + }); + + it('should return errors for non-existent beats and tags', async () => { + const nonExistentBeatId = chance.word(); + const nonExistentTag = chance.word(); + + const apiResponse = await beatsLib.assignTagsToBeats(fakeReq, [ + { beatId: nonExistentBeatId, tag: nonExistentTag }, + ]); + + expect(apiResponse.assignments).to.eql([ + { + result: `Beat ${nonExistentBeatId} and tag ${nonExistentTag} not found`, + status: 404, + }, + ]); + + const beat = beatsDB.find(b => b.id === 'bar') as any; + expect(beat).to.not.have.property('tags'); + }); + }); +}); diff --git a/x-pack/plugins/beats/server/lib/domains/__tests__/beats/enroll.test.ts b/x-pack/plugins/beats/server/lib/domains/__tests__/beats/enroll.test.ts new file mode 100644 index 0000000000000..f52f1096227c0 --- /dev/null +++ b/x-pack/plugins/beats/server/lib/domains/__tests__/beats/enroll.test.ts @@ -0,0 +1,136 @@ +/* + * 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 { MemoryBeatsAdapter } from '../../../adapters/beats/memory_beats_adapter'; +import { TestingBackendFrameworkAdapter } from '../../../adapters/famework/kibana/testing_framework_adapter'; +import { MemoryTagsAdapter } from '../../../adapters/tags/memory_tags_adapter'; +import { MemoryTokensAdapter } from '../../../adapters/tokens/memory_tokens_adapter'; + +import { BeatTag, CMBeat, EnrollmentToken } from './../../../lib'; + +import { CMBeatsDomain } from '../../beats'; +import { CMTagsDomain } from '../../tags'; +import { CMTokensDomain } from '../../tokens'; + +import Chance from 'chance'; +import { sign as signToken } from 'jsonwebtoken'; +import { omit } from 'lodash'; +import moment from 'moment'; + +const seed = Date.now(); +const chance = new Chance(seed); + +const settings = { + encryptionKey: 'something_who_cares', + enrollmentTokensTtlInSeconds: 10 * 60, // 10 minutes +}; + +describe('Beats Domain Lib', () => { + let beatsLib: CMBeatsDomain; + let tokensLib: CMTokensDomain; + + let beatsDB: CMBeat[] = []; + let tagsDB: BeatTag[] = []; + let tokensDB: EnrollmentToken[] = []; + let validEnrollmentToken: string; + let beatId: string; + let beat: Partial; + + describe('enroll_beat', () => { + beforeEach(async () => { + validEnrollmentToken = chance.word(); + beatId = chance.word(); + + beatsDB = []; + tagsDB = []; + tokensDB = [ + { + expires_on: moment() + .add(4, 'hours') + .toJSON(), + token: validEnrollmentToken, + }, + ]; + + const version = + chance.integer({ min: 1, max: 10 }) + + '.' + + chance.integer({ min: 1, max: 10 }) + + '.' + + chance.integer({ min: 1, max: 10 }); + + beat = { + host_name: 'foo.bar.com', + type: 'filebeat', + version, + }; + + const framework = new TestingBackendFrameworkAdapter(null, settings); + + tokensLib = new CMTokensDomain(new MemoryTokensAdapter(tokensDB), { + framework, + }); + + const tagsLib = new CMTagsDomain(new MemoryTagsAdapter(tagsDB)); + + beatsLib = new CMBeatsDomain(new MemoryBeatsAdapter(beatsDB), { + tags: tagsLib, + tokens: tokensLib, + }); + }); + + it('should enroll beat, returning an access token', async () => { + const { token } = await tokensLib.getEnrollmentToken( + validEnrollmentToken + ); + + expect(token).to.equal(validEnrollmentToken); + const { accessToken } = await beatsLib.enrollBeat( + beatId, + '192.168.1.1', + omit(beat, 'enrollment_token') + ); + + expect(beatsDB.length).to.eql(1); + expect(beatsDB[0]).to.have.property('host_ip'); + + expect(accessToken).to.eql(beatsDB[0].access_token); + + await tokensLib.deleteEnrollmentToken(validEnrollmentToken); + + expect(tokensDB.length).to.eql(0); + }); + + it('should reject an invalid enrollment token', async () => { + const { token } = await tokensLib.getEnrollmentToken(chance.word()); + + expect(token).to.eql(null); + }); + + it('should reject an expired enrollment token', async () => { + const { token } = await tokensLib.getEnrollmentToken( + signToken({}, settings.encryptionKey, { + expiresIn: '-1min', + }) + ); + + expect(token).to.eql(null); + }); + + it('should delete the given enrollment token so it may not be reused', async () => { + expect(tokensDB[0].token).to.eql(validEnrollmentToken); + await tokensLib.deleteEnrollmentToken(validEnrollmentToken); + expect(tokensDB.length).to.eql(0); + + const { token } = await tokensLib.getEnrollmentToken( + validEnrollmentToken + ); + + expect(token).to.eql(null); + }); + }); +}); diff --git a/x-pack/plugins/beats/server/lib/domains/__tests__/beats/verify.test.ts b/x-pack/plugins/beats/server/lib/domains/__tests__/beats/verify.test.ts new file mode 100644 index 0000000000000..7310b101a351a --- /dev/null +++ b/x-pack/plugins/beats/server/lib/domains/__tests__/beats/verify.test.ts @@ -0,0 +1,192 @@ +/* + * 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 { wrapRequest } from '../../../../utils/wrap_request'; +import { MemoryBeatsAdapter } from '../../../adapters/beats/memory_beats_adapter'; +import { TestingBackendFrameworkAdapter } from '../../../adapters/famework/kibana/testing_framework_adapter'; +import { MemoryTagsAdapter } from '../../../adapters/tags/memory_tags_adapter'; +import { MemoryTokensAdapter } from '../../../adapters/tokens/memory_tokens_adapter'; + +import { BeatTag, CMBeat, EnrollmentToken } from './../../../lib'; + +import { CMBeatsDomain } from '../../beats'; +import { CMTagsDomain } from '../../tags'; +import { CMTokensDomain } from '../../tokens'; + +import Chance from 'chance'; + +const seed = Date.now(); +const chance = new Chance(seed); + +const settings = { + encryptionKey: 'something_who_cares', + enrollmentTokensTtlInSeconds: 10 * 60, // 10 minutes +}; + +const fakeReq = wrapRequest({ + headers: {}, + info: {}, + params: {}, + payload: {}, + query: {}, +}); + +describe('Beats Domain Lib', () => { + let beatsLib: CMBeatsDomain; + let tokensLib: CMTokensDomain; + + let beatsDB: CMBeat[] = []; + let tagsDB: BeatTag[] = []; + let tokensDB: EnrollmentToken[] = []; + + describe('verify_beat', () => { + beforeEach(async () => { + beatsDB = [ + { + access_token: '9a6c99ae0fd84b068819701169cd8a4b', + host_ip: '1.2.3.4', + host_name: 'foo.bar.com', + id: 'qux', + type: 'filebeat', + }, + { + access_token: '188255eb560a4448b72656c5e99cae6f', + host_ip: '22.33.11.44', + host_name: 'baz.bar.com', + id: 'baz', + type: 'metricbeat', + }, + { + access_token: '93c4a4dd08564c189a7ec4e4f046b975', + host_ip: '1.2.3.4', + host_name: 'foo.bar.com', + id: 'foo', + tags: ['production', 'qa'], + type: 'metricbeat', + verified_on: '2018-05-15T16:25:38.924Z', + }, + { + access_token: '3c4a4dd08564c189a7ec4e4f046b9759', + host_ip: '11.22.33.44', + host_name: 'foo.com', + id: 'bar', + type: 'filebeat', + }, + ]; + tagsDB = [ + { + configuration_blocks: [], + id: 'production', + }, + { + configuration_blocks: [], + id: 'development', + }, + { + configuration_blocks: [], + id: 'qa', + }, + ]; + tokensDB = []; + + const framework = new TestingBackendFrameworkAdapter(null, settings); + + tokensLib = new CMTokensDomain(new MemoryTokensAdapter(tokensDB), { + framework, + }); + + const tagsLib = new CMTagsDomain(new MemoryTagsAdapter(tagsDB)); + + beatsLib = new CMBeatsDomain(new MemoryBeatsAdapter(beatsDB), { + tags: tagsLib, + tokens: tokensLib, + }); + }); + + it('should return errors for non-existent beats', async () => { + const nonExistentBeatId = chance.word(); + + interface Beats { + id: string; + status?: number; + result?: string; + } + + const beats: Beats[] = [{ id: 'bar' }, { id: nonExistentBeatId }]; + const beatIds = beats.map(b => b.id); + + const { + verifiedBeatIds, + alreadyVerifiedBeatIds, + nonExistentBeatIds, + } = await beatsLib.verifyBeats(fakeReq, beatIds); + + // TODO calculation of status should be done in-lib, w/switch statement here + beats.forEach(b => { + if (nonExistentBeatIds.includes(b.id)) { + b.status = 404; + b.result = 'not found'; + } else if (alreadyVerifiedBeatIds.includes(b.id)) { + b.status = 200; + b.result = 'already verified'; + } else if (verifiedBeatIds.includes(b.id)) { + b.status = 200; + b.result = 'verified'; + } else { + b.status = 400; + b.result = 'not verified'; + } + }); + + const response = { beats }; + expect(response.beats).to.eql([ + { id: 'bar', status: 200, result: 'verified' }, + { id: nonExistentBeatId, status: 404, result: 'not found' }, + ]); + }); + + it('should not re-verify already-verified beats', async () => { + interface Beats { + id: string; + status?: number; + result?: string; + } + + const beats: Beats[] = [{ id: 'foo' }, { id: 'bar' }]; + const beatIds = beats.map(b => b.id); + + const { + verifiedBeatIds, + alreadyVerifiedBeatIds, + nonExistentBeatIds, + } = await beatsLib.verifyBeats(fakeReq, beatIds); + + // TODO calculation of status should be done in-lib, w/switch statement here + beats.forEach(beat => { + if (nonExistentBeatIds.includes(beat.id)) { + beat.status = 404; + beat.result = 'not found'; + } else if (alreadyVerifiedBeatIds.includes(beat.id)) { + beat.status = 200; + beat.result = 'already verified'; + } else if (verifiedBeatIds.includes(beat.id)) { + beat.status = 200; + beat.result = 'verified'; + } else { + beat.status = 400; + beat.result = 'not verified'; + } + }); + + const response = { beats }; + expect(response.beats).to.eql([ + { id: 'foo', status: 200, result: 'already verified' }, + { id: 'bar', status: 200, result: 'verified' }, + ]); + }); + }); +}); diff --git a/x-pack/plugins/beats/server/lib/domains/__tests__/tokens.test.ts b/x-pack/plugins/beats/server/lib/domains/__tests__/tokens.test.ts new file mode 100644 index 0000000000000..174c7d628778c --- /dev/null +++ b/x-pack/plugins/beats/server/lib/domains/__tests__/tokens.test.ts @@ -0,0 +1,87 @@ +/* + * 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 { wrapRequest } from '../../../utils/wrap_request'; +import { TestingBackendFrameworkAdapter } from '../../adapters/famework/kibana/testing_framework_adapter'; +import { MemoryTokensAdapter } from '../../adapters/tokens/memory_tokens_adapter'; +import { EnrollmentToken } from '../../lib'; +import { CMTokensDomain } from '../tokens'; + +import Chance from 'chance'; +import moment from 'moment'; + +const seed = Date.now(); +const chance = new Chance(seed); + +const fakeReq = wrapRequest({ + headers: {}, + info: {}, + params: {}, + payload: {}, + query: {}, +}); + +const settings = { + encryptionKey: 'something_who_cares', + enrollmentTokensTtlInSeconds: 10 * 60, // 10 minutes +}; + +describe('Token Domain Lib', () => { + let tokensLib: CMTokensDomain; + let tokensDB: EnrollmentToken[] = []; + + beforeEach(async () => { + tokensDB = []; + const framework = new TestingBackendFrameworkAdapter(null, settings); + + tokensLib = new CMTokensDomain(new MemoryTokensAdapter(tokensDB), { + framework, + }); + }); + + it('should generate webtokens with a qty of 1', async () => { + const tokens = await tokensLib.createEnrollmentTokens(fakeReq, 1); + + expect(tokens.length).to.be(1); + + expect(typeof tokens[0]).to.be('string'); + }); + + it('should create the specified number of tokens', async () => { + const numTokens = chance.integer({ min: 1, max: 20 }); + const tokensFromApi = await tokensLib.createEnrollmentTokens( + fakeReq, + numTokens + ); + + expect(tokensFromApi.length).to.eql(numTokens); + expect(tokensFromApi).to.eql(tokensDB.map((t: EnrollmentToken) => t.token)); + }); + + it('should set token expiration to 10 minutes from now by default', async () => { + await tokensLib.createEnrollmentTokens(fakeReq, 1); + + const token = tokensDB[0]; + + // We do a fuzzy check to see if the token expires between 9 and 10 minutes + // from now because a bit of time has elapsed been the creation of the + // tokens and this check. + const tokenExpiresOn = moment(token.expires_on).valueOf(); + + // Because sometimes the test runs so fast it it equal, and we dont use expect.js version that has toBeLessThanOrEqualTo + const tenMinutesFromNow = moment() + .add('10', 'minutes') + .add('1', 'seconds') + .valueOf(); + + const almostTenMinutesFromNow = moment(tenMinutesFromNow) + .subtract('2', 'seconds') + .valueOf(); + expect(tokenExpiresOn).to.be.lessThan(tenMinutesFromNow); + expect(tokenExpiresOn).to.be.greaterThan(almostTenMinutesFromNow); + }); +}); diff --git a/x-pack/plugins/beats/server/lib/domains/beats.ts b/x-pack/plugins/beats/server/lib/domains/beats.ts index c0d9ec704e2b1..0d5e068ff4ff7 100644 --- a/x-pack/plugins/beats/server/lib/domains/beats.ts +++ b/x-pack/plugins/beats/server/lib/domains/beats.ts @@ -11,7 +11,6 @@ */ import { uniq } from 'lodash'; -import uuid from 'uuid'; import { findNonExistentItems } from '../../utils/find_non_existent_items'; import { @@ -45,15 +44,16 @@ export class CMBeatsDomain { ) { const beat = await this.adapter.get(beatId); + const { verified: isAccessTokenValid } = this.tokens.verifyToken( + beat ? beat.access_token : '', + accessToken + ); + // TODO make return type enum if (beat === null) { return 'beat-not-found'; } - const isAccessTokenValid = this.tokens.areTokensEqual( - beat.access_token, - accessToken - ); if (!isAccessTokenValid) { return 'invalid-access-token'; } @@ -74,8 +74,7 @@ export class CMBeatsDomain { remoteAddress: string, beat: Partial ) { - // TODO move this to the token lib - const accessToken = uuid.v4().replace(/-/g, ''); + const accessToken = this.tokens.generateAccessToken(); await this.adapter.insert({ ...beat, access_token: accessToken, @@ -136,37 +135,28 @@ export class CMBeatsDomain { // TODO cleanup return value, should return a status enum public async verifyBeats(req: FrameworkRequest, beatIds: string[]) { - const beatsFromEs = await this.adapter.getVerifiedWithIds(req, beatIds); - - const nonExistentBeatIds = beatsFromEs.reduce( - (nonExistentIds: any, beatFromEs: any, idx: any) => { - if (!beatFromEs.found) { - nonExistentIds.push(beatIds[idx]); - } - return nonExistentIds; - }, - [] - ); + const beatsFromEs = await this.adapter.getWithIds(req, beatIds); + + const nonExistentBeatIds = findNonExistentItems(beatsFromEs, beatIds); const alreadyVerifiedBeatIds = beatsFromEs - .filter((beat: any) => beat.found) - .filter((beat: any) => beat._source.beat.hasOwnProperty('verified_on')) - .map((beat: any) => beat._source.beat.id); + .filter((beat: any) => beat.hasOwnProperty('verified_on')) + .map((beat: any) => beat.id); const toBeVerifiedBeatIds = beatsFromEs - .filter((beat: any) => beat.found) - .filter((beat: any) => !beat._source.beat.hasOwnProperty('verified_on')) - .map((beat: any) => beat._source.beat.id); + .filter((beat: any) => !beat.hasOwnProperty('verified_on')) + .map((beat: any) => beat.id); const verifications = await this.adapter.verifyBeats( req, toBeVerifiedBeatIds ); + return { alreadyVerifiedBeatIds, nonExistentBeatIds, toBeVerifiedBeatIds, - verifications, + verifiedBeatIds: verifications.map((v: any) => v.id), }; } @@ -182,7 +172,6 @@ export class CMBeatsDomain { }; const beats = await this.adapter.getWithIds(req, beatIds); const tags = await this.tags.getTagsWithIds(req, tagIds); - // Handle assignments containing non-existing beat IDs or tags const nonExistentBeatIds = findNonExistentItems(beats, beatIds); const nonExistentTags = findNonExistentItems(tags, tagIds); diff --git a/x-pack/plugins/beats/server/lib/domains/tokens.ts b/x-pack/plugins/beats/server/lib/domains/tokens.ts index 6e55d78ecdcc8..b2a9d283e484a 100644 --- a/x-pack/plugins/beats/server/lib/domains/tokens.ts +++ b/x-pack/plugins/beats/server/lib/domains/tokens.ts @@ -5,6 +5,7 @@ */ import { timingSafeEqual } from 'crypto'; +import { sign as signToken, verify as verifyToken } from 'jsonwebtoken'; import moment from 'moment'; import uuid from 'uuid'; import { CMTokensAdapter, FrameworkRequest } from '../lib'; @@ -26,32 +27,96 @@ export class CMTokensDomain { } public async getEnrollmentToken(enrollmentToken: string) { - return await this.adapter.getEnrollmentToken(enrollmentToken); + const fullToken = await this.adapter.getEnrollmentToken(enrollmentToken); + + if (!fullToken) { + return { + token: null, + expired: true, + expires_on: null, + }; + } + + const { verified, expired } = this.verifyToken( + enrollmentToken, + fullToken.token || '', + false + ); + + if (!verified) { + return { + expired, + token: null, + expires_on: null, + }; + } + + return { ...fullToken, expired }; } public async deleteEnrollmentToken(enrollmentToken: string) { return await this.adapter.deleteEnrollmentToken(enrollmentToken); } - public areTokensEqual(token1: string, token2: string) { + public verifyToken(recivedToken: string, token2: string, decode = true) { + let tokenDecoded = true; + let expired = false; + + if (decode) { + const enrollmentTokenSecret = this.framework.getSetting( + 'xpack.beats.encryptionKey' + ); + + try { + verifyToken(recivedToken, enrollmentTokenSecret); + tokenDecoded = true; + } catch (err) { + if (err.name === 'TokenExpiredError') { + expired = true; + } + tokenDecoded = false; + } + } + if ( - typeof token1 !== 'string' || + typeof recivedToken !== 'string' || typeof token2 !== 'string' || - token1.length !== token2.length + recivedToken.length !== token2.length ) { // This prevents a more subtle timing attack where we know already the tokens aren't going to // match but still we don't return fast. Instead we compare two pre-generated random tokens using // the same comparison algorithm that we would use to compare two equal-length tokens. - return timingSafeEqual( - Buffer.from(RANDOM_TOKEN_1, 'utf8'), - Buffer.from(RANDOM_TOKEN_2, 'utf8') - ); + return { + expired, + verified: + timingSafeEqual( + Buffer.from(RANDOM_TOKEN_1, 'utf8'), + Buffer.from(RANDOM_TOKEN_2, 'utf8') + ) && tokenDecoded, + }; } - return timingSafeEqual( - Buffer.from(token1, 'utf8'), - Buffer.from(token2, 'utf8') + return { + expired, + verified: + timingSafeEqual( + Buffer.from(recivedToken, 'utf8'), + Buffer.from(token2, 'utf8') + ) && tokenDecoded, + }; + } + + public generateAccessToken() { + const enrollmentTokenSecret = this.framework.getSetting( + 'xpack.beats.encryptionKey' ); + + const tokenData = { + created: moment().toJSON(), + randomHash: this.createRandomHash(), + }; + + return signToken(tokenData, enrollmentTokenSecret); } public async createEnrollmentTokens( @@ -62,6 +127,7 @@ export class CMTokensDomain { const enrollmentTokensTtlInSeconds = this.framework.getSetting( 'xpack.beats.enrollmentTokensTtlInSeconds' ); + const enrollmentTokenExpiration = moment() .add(enrollmentTokensTtlInSeconds, 'seconds') .toJSON(); @@ -69,7 +135,7 @@ export class CMTokensDomain { while (tokens.length < numTokens) { tokens.push({ expires_on: enrollmentTokenExpiration, - token: uuid.v4().replace(/-/g, ''), + token: this.createRandomHash(), }); } @@ -77,4 +143,8 @@ export class CMTokensDomain { return tokens.map(token => token.token); } + + private createRandomHash() { + return uuid.v4().replace(/-/g, ''); + } } diff --git a/x-pack/plugins/beats/server/lib/lib.ts b/x-pack/plugins/beats/server/lib/lib.ts index 37d0a989e4cf5..6aab0acd733d8 100644 --- a/x-pack/plugins/beats/server/lib/lib.ts +++ b/x-pack/plugins/beats/server/lib/lib.ts @@ -43,16 +43,16 @@ export interface ConfigurationBlock { export interface CMBeat { id: string; access_token: string; - verified_on: string; + verified_on?: string; type: string; - version: string; + version?: string; host_ip: string; host_name: string; - ephemeral_id: string; - local_configuration_yml: string; - tags: string; - central_configuration_yml: string; - metadata: {}; + ephemeral_id?: string; + local_configuration_yml?: string; + tags?: string[]; + central_configuration_yml?: string; + metadata?: {}; } export interface BeatTag { @@ -68,7 +68,10 @@ export interface EnrollmentToken { export interface CMTokensAdapter { deleteEnrollmentToken(enrollmentToken: string): Promise; getEnrollmentToken(enrollmentToken: string): Promise; - upsertTokens(req: FrameworkRequest, tokens: EnrollmentToken[]): Promise; + upsertTokens( + req: FrameworkRequest, + tokens: EnrollmentToken[] + ): Promise; } // FIXME: fix getTagsWithIds return type @@ -84,7 +87,6 @@ export interface CMBeatsAdapter { get(id: string): any; getAll(req: FrameworkRequest): any; getWithIds(req: FrameworkRequest, beatIds: string[]): any; - getVerifiedWithIds(req: FrameworkRequest, beatIds: string[]): any; verifyBeats(req: FrameworkRequest, beatIds: string[]): any; removeTagsFromBeats( req: FrameworkRequest, @@ -108,7 +110,7 @@ export interface CMTagAssignment { export interface BackendFrameworkAdapter { version: string; - getSetting(settingPath: string): string | number; + getSetting(settingPath: string): any; exposeStaticDir(urlPath: string, dir: string): void; installIndexTemplate(name: string, template: {}): void; registerRoute( diff --git a/x-pack/plugins/beats/server/rest_api/beats/enroll.ts b/x-pack/plugins/beats/server/rest_api/beats/enroll.ts index fe154592564ae..c86e5272e1e23 100644 --- a/x-pack/plugins/beats/server/rest_api/beats/enroll.ts +++ b/x-pack/plugins/beats/server/rest_api/beats/enroll.ts @@ -33,17 +33,16 @@ export const createBeatEnrollmentRoute = (libs: CMServerLibs) => ({ const enrollmentToken = request.headers['kbn-beats-enrollment-token']; try { - const { - token, - expires_on: expiresOn, - } = await libs.tokens.getEnrollmentToken(enrollmentToken); + const { token, expires_on } = await libs.tokens.getEnrollmentToken( + enrollmentToken + ); + if (expires_on && moment(expires_on).isBefore(moment())) { + return reply({ message: 'Expired enrollment token' }).code(400); + } if (!token) { return reply({ message: 'Invalid enrollment token' }).code(400); } - if (moment(expiresOn).isBefore(moment())) { - return reply({ message: 'Expired enrollment token' }).code(400); - } const { accessToken } = await libs.beats.enrollBeat( beatId, request.info.remoteAddress, diff --git a/x-pack/plugins/beats/server/rest_api/beats/update.ts b/x-pack/plugins/beats/server/rest_api/beats/update.ts index 41d403399d45f..3683c02ca2ccb 100644 --- a/x-pack/plugins/beats/server/rest_api/beats/update.ts +++ b/x-pack/plugins/beats/server/rest_api/beats/update.ts @@ -15,7 +15,7 @@ export const createBeatUpdateRoute = (libs: CMServerLibs) => ({ auth: false, validate: { headers: Joi.object({ - 'kbn-beats-access-token': Joi.string().required(), + 'kbn-beats-access-token': Joi.string(), }).options({ allowUnknown: true, }), diff --git a/x-pack/plugins/beats/server/rest_api/beats/verify.ts b/x-pack/plugins/beats/server/rest_api/beats/verify.ts index 866fa77d0c337..7dba7f4e20692 100644 --- a/x-pack/plugins/beats/server/rest_api/beats/verify.ts +++ b/x-pack/plugins/beats/server/rest_api/beats/verify.ts @@ -29,22 +29,11 @@ export const createBeatVerificationRoute = (libs: CMServerLibs) => ({ try { const { - verifications, + verifiedBeatIds, alreadyVerifiedBeatIds, - toBeVerifiedBeatIds, nonExistentBeatIds, } = await libs.beats.verifyBeats(request, beatIds); - const verifiedBeatIds = verifications.reduce( - (verifiedBeatList: any, verification: any, idx: any) => { - if (verification.update.status === 200) { - verifiedBeatList.push(toBeVerifiedBeatIds[idx]); - } - return verifiedBeatList; - }, - [] - ); - // TODO calculation of status should be done in-lib, w/switch statement here beats.forEach(beat => { if (nonExistentBeatIds.includes(beat.id)) { diff --git a/x-pack/plugins/beats/server/utils/error_wrappers/wrap_es_error.test.js b/x-pack/plugins/beats/server/utils/error_wrappers/wrap_es_error.test.js index 03b04a2ef61d2..de79815258f7a 100644 --- a/x-pack/plugins/beats/server/utils/error_wrappers/wrap_es_error.test.js +++ b/x-pack/plugins/beats/server/utils/error_wrappers/wrap_es_error.test.js @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import expect from 'expect.js'; import { wrapEsError } from './wrap_es_error'; describe('wrap_es_error', () => { @@ -34,7 +35,7 @@ describe('wrap_es_error', () => { expect(wrappedError.isBoom).to.be(true); expect(wrappedError.message).to.be( - 'Insufficient user permissions for managing Logstash pipelines' + 'Insufficient user permissions for managing Beats configuration' ); }); }); diff --git a/x-pack/plugins/beats/server/utils/find_non_existent_items.ts b/x-pack/plugins/beats/server/utils/find_non_existent_items.ts index 53e4066acc879..d6b2a0c9e143b 100644 --- a/x-pack/plugins/beats/server/utils/find_non_existent_items.ts +++ b/x-pack/plugins/beats/server/utils/find_non_existent_items.ts @@ -4,11 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -export function findNonExistentItems(items: any, requestedItems: any) { - return items.reduce((nonExistentItems: any, item: any, idx: any) => { - if (!item.found) { - nonExistentItems.push(requestedItems[idx]); - } - return nonExistentItems; - }, []); +interface RandomItem { + id: string; + [key: string]: any; +} + +export function findNonExistentItems(items: RandomItem[], requestedItems: any) { + return requestedItems.reduce( + (nonExistentItems: string[], requestedItem: string, idx: number) => { + if ( + items.findIndex( + (item: RandomItem) => item && item.id === requestedItem + ) === -1 + ) { + nonExistentItems.push(requestedItems[idx]); + } + return nonExistentItems; + }, + [] + ); } diff --git a/x-pack/plugins/beats/wallaby.js b/x-pack/plugins/beats/wallaby.js index c20488d35cfb6..8c0c4aa355925 100644 --- a/x-pack/plugins/beats/wallaby.js +++ b/x-pack/plugins/beats/wallaby.js @@ -3,15 +3,18 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +const path = require('path'); +process.env.NODE_PATH = path.join(__dirname, '..', '..', 'node_modules'); module.exports = function (wallaby) { return { debug: true, files: [ - '../../tsconfig.json', + './tsconfig.json', //'plugins/beats/public/**/*.+(js|jsx|ts|tsx|json|snap|css|less|sass|scss|jpg|jpeg|gif|png|svg)', 'server/**/*.+(js|jsx|ts|tsx|json|snap|css|less|sass|scss|jpg|jpeg|gif|png|svg)', 'common/**/*.+(js|jsx|ts|tsx|json|snap|css|less|sass|scss|jpg|jpeg|gif|png|svg)', + '!**/*.test.ts', ], tests: ['**/*.test.ts'], @@ -22,6 +25,40 @@ module.exports = function (wallaby) { testFramework: 'jest', compilers: { '**/*.ts?(x)': wallaby.compilers.typeScript({ module: 'commonjs' }), + '**/*.js': wallaby.compilers.babel({ + babelrc: false, + presets: [require.resolve('@kbn/babel-preset/node_preset')], + }), + }, + setup: wallaby => { + const path = require('path'); + + const kibanaDirectory = path.resolve( + wallaby.localProjectDir, + '..', + '..', + '..' + ); + wallaby.testFramework.configure({ + rootDir: wallaby.localProjectDir, + moduleNameMapper: { + '^ui/(.*)': `${kibanaDirectory}/src/ui/public/$1`, + // eslint-disable-next-line + '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': `${kibanaDirectory}/src/dev/jest/mocks/file_mock.js`, + '\\.(css|less|scss)$': `${kibanaDirectory}/src/dev/jest/mocks/style_mock.js`, + }, + + setupFiles: [ + `${kibanaDirectory}/x-pack/dev-tools/jest/setup/enzyme.js`, + ], + snapshotSerializers: [ + `${kibanaDirectory}/node_modules/enzyme-to-json/serializer`, + ], + transform: { + '^.+\\.js$': `${kibanaDirectory}/src/dev/jest/babel_transform.js`, + //"^.+\\.tsx?$": `${kibanaDirectory}/src/dev/jest/ts_transform.js`, + }, + }); }, }; }; diff --git a/x-pack/test/api_integration/apis/beats/enroll_beat.js b/x-pack/test/api_integration/apis/beats/enroll_beat.js index 91317bca976ee..1dde64c9ee1d8 100644 --- a/x-pack/test/api_integration/apis/beats/enroll_beat.js +++ b/x-pack/test/api_integration/apis/beats/enroll_beat.js @@ -6,10 +6,8 @@ import expect from 'expect.js'; import moment from 'moment'; -import { - ES_INDEX_NAME, - ES_TYPE_NAME -} from './constants'; + +import { ES_INDEX_NAME, ES_TYPE_NAME } from './constants'; export default function ({ getService }) { const supertest = getService('supertest'); @@ -23,17 +21,19 @@ export default function ({ getService }) { beforeEach(async () => { validEnrollmentToken = chance.word(); + beatId = chance.word(); - const version = chance.integer({ min: 1, max: 10 }) - + '.' - + chance.integer({ min: 1, max: 10 }) - + '.' - + chance.integer({ min: 1, max: 10 }); + const version = + chance.integer({ min: 1, max: 10 }) + + '.' + + chance.integer({ min: 1, max: 10 }) + + '.' + + chance.integer({ min: 1, max: 10 }); beat = { type: 'filebeat', host_name: 'foo.bar.com', - version + version, }; await es.index({ @@ -44,17 +44,17 @@ export default function ({ getService }) { type: 'enrollment_token', enrollment_token: { token: validEnrollmentToken, - expires_on: moment().add(4, 'hours').toJSON() - } - } + expires_on: moment() + .add(4, 'hours') + .toJSON(), + }, + }, }); }); it('should enroll beat in an unverified state', async () => { await supertest - .post( - `/api/beats/agent/${beatId}` - ) + .post(`/api/beats/agent/${beatId}`) .set('kbn-xsrf', 'xxx') .set('kbn-beats-enrollment-token', validEnrollmentToken) .send(beat) @@ -63,7 +63,7 @@ export default function ({ getService }) { const esResponse = await es.get({ index: ES_INDEX_NAME, type: ES_TYPE_NAME, - id: `beat:${beatId}` + id: `beat:${beatId}`, }); expect(esResponse._source.beat).to.not.have.property('verified_on'); @@ -72,9 +72,7 @@ export default function ({ getService }) { it('should contain an access token in the response', async () => { const { body: apiResponse } = await supertest - .post( - `/api/beats/agent/${beatId}` - ) + .post(`/api/beats/agent/${beatId}`) .set('kbn-xsrf', 'xxx') .set('kbn-beats-enrollment-token', validEnrollmentToken) .send(beat) @@ -85,7 +83,7 @@ export default function ({ getService }) { const esResponse = await es.get({ index: ES_INDEX_NAME, type: ES_TYPE_NAME, - id: `beat:${beatId}` + id: `beat:${beatId}`, }); const accessTokenInEs = esResponse._source.beat.access_token; @@ -96,9 +94,7 @@ export default function ({ getService }) { it('should reject an invalid enrollment token', async () => { const { body: apiResponse } = await supertest - .post( - `/api/beats/agent/${beatId}` - ) + .post(`/api/beats/agent/${beatId}`) .set('kbn-xsrf', 'xxx') .set('kbn-beats-enrollment-token', chance.word()) .send(beat) @@ -108,7 +104,10 @@ export default function ({ getService }) { }); it('should reject an expired enrollment token', async () => { - const expiredEnrollmentToken = chance.word(); + const expiredEnrollmentToken = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.' + + 'eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1LCJleHAiOjE1MzAzMzAxMzV9.' + + 'Azf4czAwWZEflR7Pf8pi-DUTcve9xyxWyViNYeUSGog'; await es.index({ index: ES_INDEX_NAME, @@ -118,15 +117,15 @@ export default function ({ getService }) { type: 'enrollment_token', enrollment_token: { token: expiredEnrollmentToken, - expires_on: moment().subtract(1, 'minute').toJSON() - } - } + expires_on: moment() + .subtract(1, 'minute') + .toJSON(), + }, + }, }); const { body: apiResponse } = await supertest - .post( - `/api/beats/agent/${beatId}` - ) + .post(`/api/beats/agent/${beatId}`) .set('kbn-xsrf', 'xxx') .set('kbn-beats-enrollment-token', expiredEnrollmentToken) .send(beat) @@ -137,9 +136,7 @@ export default function ({ getService }) { it('should delete the given enrollment token so it may not be reused', async () => { await supertest - .post( - `/api/beats/agent/${beatId}` - ) + .post(`/api/beats/agent/${beatId}`) .set('kbn-xsrf', 'xxx') .set('kbn-beats-enrollment-token', validEnrollmentToken) .send(beat) @@ -149,7 +146,7 @@ export default function ({ getService }) { index: ES_INDEX_NAME, type: ES_TYPE_NAME, id: `enrollment_token:${validEnrollmentToken}`, - ignore: [ 404 ] + ignore: [404], }); expect(esResponse.found).to.be(false); @@ -157,9 +154,7 @@ export default function ({ getService }) { it('should fail if the beat with the same ID is enrolled twice', async () => { await supertest - .post( - `/api/beats/agent/${beatId}` - ) + .post(`/api/beats/agent/${beatId}`) .set('kbn-xsrf', 'xxx') .set('kbn-beats-enrollment-token', validEnrollmentToken) .send(beat) @@ -173,15 +168,15 @@ export default function ({ getService }) { type: 'enrollment_token', enrollment_token: { token: validEnrollmentToken, - expires_on: moment().add(4, 'hours').toJSON() - } - } + expires_on: moment() + .add(4, 'hours') + .toJSON(), + }, + }, }); await supertest - .post( - `/api/beats/agent/${beatId}` - ) + .post(`/api/beats/agent/${beatId}`) .set('kbn-xsrf', 'xxx') .set('kbn-beats-enrollment-token', validEnrollmentToken) .send(beat) diff --git a/x-pack/test/api_integration/apis/beats/update_beat.js b/x-pack/test/api_integration/apis/beats/update_beat.js index 92e5771e0ef4b..fb30970d3cc7f 100644 --- a/x-pack/test/api_integration/apis/beats/update_beat.js +++ b/x-pack/test/api_integration/apis/beats/update_beat.js @@ -5,10 +5,8 @@ */ import expect from 'expect.js'; -import { - ES_INDEX_NAME, - ES_TYPE_NAME -} from './constants'; +import { ES_INDEX_NAME, ES_TYPE_NAME } from './constants'; +import moment from 'moment'; export default function ({ getService }) { const supertest = getService('supertest'); @@ -17,23 +15,44 @@ export default function ({ getService }) { const esArchiver = getService('esArchiver'); describe('update_beat', () => { + let validEnrollmentToken; let beat; const archive = 'beats/list'; beforeEach('load beats archive', () => esArchiver.load(archive)); - beforeEach(() => { - const version = chance.integer({ min: 1, max: 10 }) - + '.' - + chance.integer({ min: 1, max: 10 }) - + '.' - + chance.integer({ min: 1, max: 10 }); + beforeEach(async () => { + validEnrollmentToken = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.' + + 'eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1fQ.' + + 'SSsX2Byyo1B1bGxV8C3G4QldhE5iH87EY_1r21-bwbI'; + const version = + chance.integer({ min: 1, max: 10 }) + + '.' + + chance.integer({ min: 1, max: 10 }) + + '.' + + chance.integer({ min: 1, max: 10 }); beat = { type: `${chance.word()}beat`, host_name: `www.${chance.word()}.net`, version, - ephemeral_id: chance.word() + ephemeral_id: chance.word(), }; + + 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(), + }, + }, + }); }); afterEach('unload beats archive', () => esArchiver.unload(archive)); @@ -41,18 +60,16 @@ export default function ({ getService }) { it('should update an existing verified beat', async () => { const beatId = 'foo'; await supertest - .put( - `/api/beats/agent/${beatId}` - ) + .put(`/api/beats/agent/${beatId}`) .set('kbn-xsrf', 'xxx') - .set('kbn-beats-access-token', '93c4a4dd08564c189a7ec4e4f046b975') + .set('kbn-beats-access-token', validEnrollmentToken) .send(beat) .expect(204); const beatInEs = await es.get({ index: ES_INDEX_NAME, type: ES_TYPE_NAME, - id: `beat:${beatId}` + id: `beat:${beatId}`, }); expect(beatInEs._source.beat.id).to.be(beatId); @@ -65,9 +82,7 @@ export default function ({ getService }) { it('should return an error for an invalid access token', async () => { const beatId = 'foo'; const { body } = await supertest - .put( - `/api/beats/agent/${beatId}` - ) + .put(`/api/beats/agent/${beatId}`) .set('kbn-xsrf', 'xxx') .set('kbn-beats-access-token', chance.word()) .send(beat) @@ -78,7 +93,7 @@ export default function ({ getService }) { const beatInEs = await es.get({ index: ES_INDEX_NAME, type: ES_TYPE_NAME, - id: `beat:${beatId}` + id: `beat:${beatId}`, }); expect(beatInEs._source.beat.id).to.be(beatId); @@ -90,12 +105,16 @@ export default function ({ getService }) { it('should return an error for an existing but unverified beat', async () => { const beatId = 'bar'; + const { body } = await supertest - .put( - `/api/beats/agent/${beatId}` - ) + .put(`/api/beats/agent/${beatId}`) .set('kbn-xsrf', 'xxx') - .set('kbn-beats-access-token', '3c4a4dd08564c189a7ec4e4f046b9759') + .set( + 'kbn-beats-access-token', + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.' + + 'eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1fQ.' + + 'SSsX2Byyo1B1bGxV8C3G4QldhE5iH87EY_1r21-bwbI' + ) .send(beat) .expect(400); @@ -104,7 +123,7 @@ export default function ({ getService }) { const beatInEs = await es.get({ index: ES_INDEX_NAME, type: ES_TYPE_NAME, - id: `beat:${beatId}` + id: `beat:${beatId}`, }); expect(beatInEs._source.beat.id).to.be(beatId); @@ -117,11 +136,9 @@ export default function ({ getService }) { it('should return an error for a non-existent beat', async () => { const beatId = chance.word(); const { body } = await supertest - .put( - `/api/beats/agent/${beatId}` - ) + .put(`/api/beats/agent/${beatId}`) .set('kbn-xsrf', 'xxx') - .set('kbn-beats-access-token', chance.word()) + .set('kbn-beats-access-token', validEnrollmentToken) .send(beat) .expect(404); diff --git a/x-pack/test/api_integration/apis/beats/verify_beats.js b/x-pack/test/api_integration/apis/beats/verify_beats.js index 2b085308b43d1..30521d0483c2d 100644 --- a/x-pack/test/api_integration/apis/beats/verify_beats.js +++ b/x-pack/test/api_integration/apis/beats/verify_beats.js @@ -19,15 +19,10 @@ export default function ({ getService }) { it('verify the given beats', async () => { const { body: apiResponse } = await supertest - .post( - '/api/beats/agents/verify' - ) + .post('/api/beats/agents/verify') .set('kbn-xsrf', 'xxx') .send({ - beats: [ - { id: 'bar' }, - { id: 'baz' } - ] + beats: [{ id: 'bar' }, { id: 'baz' }], }) .expect(200); @@ -39,36 +34,26 @@ export default function ({ getService }) { it('should not re-verify already-verified beats', async () => { const { body: apiResponse } = await supertest - .post( - '/api/beats/agents/verify' - ) + .post('/api/beats/agents/verify') .set('kbn-xsrf', 'xxx') .send({ - beats: [ - { id: 'foo' }, - { id: 'bar' } - ] + beats: [{ id: 'foo' }, { id: 'bar' }], }) .expect(200); expect(apiResponse.beats).to.eql([ { id: 'foo', status: 200, result: 'already verified' }, - { id: 'bar', status: 200, result: 'verified' } + { id: 'bar', status: 200, result: 'verified' }, ]); }); it('should return errors for non-existent beats', async () => { const nonExistentBeatId = chance.word(); const { body: apiResponse } = await supertest - .post( - '/api/beats/agents/verify' - ) + .post('/api/beats/agents/verify') .set('kbn-xsrf', 'xxx') .send({ - beats: [ - { id: 'bar' }, - { id: nonExistentBeatId } - ] + beats: [{ id: 'bar' }, { id: nonExistentBeatId }], }) .expect(200); diff --git a/x-pack/test/functional/es_archives/beats/list/data.json.gz b/x-pack/test/functional/es_archives/beats/list/data.json.gz index b33ecf434c104716138ed08200cf49feaa9b73c7..6af0e1b8aeb47b3ac9c5bea7dba0abc92e3fe9e4 100644 GIT binary patch literal 521 zcmV+k0`~nMiwFp){WeQOZ*BnXRb6k|Fcf{yuRwV&RveO!=53&s87Wi@ z#@bF(7IM;)v?a?%|>Z7)U=4GYBf}k6ZJ|0 zE9_?y*@!@dEc9qD2_V2Bp3#7YY15@RO?LEJ2j|d2R(TSUH0wFb4`{-(m{h%MwUW7K zHTF@(s_~}Gr*F6-H|I&}ut=sM&_N3r@3J8dUdlNKE{*}=L7nrWwi3DnF(EWboRlwV zDATm)&)ptj_pFb;l?VKRKBwcikmeIqc+rI&Vv>?G`?)4^1wBXEMe9rH?+IqmW z(!Lw6?UHNug6D&gQP^b%BerJv`<;d)Hnv4xd}A9X{SOE;}n8{Fu| zQ@td`vqW%zydnaNV(w)mWunVf9e>8^YxwiB>mxqj??KGfZhxFu9&)sfEROi2j@56N$h=!*J0l4bwm^~91m=dFGL LEy(|MkO}|*NeBrP literal 447 zcmV;w0YLsAiwFqO9SBQOZ*BnHRLgGTFbur=D-53-!Ez);@^|#qVqs7c zoi!R;sUJahgZ%qSP6E5wCOveK0DCb&a)zLW93P@MPWoS4O!7Ff&LmGEv4hPJG6x^{ zuxc#s1Ax@fz#408`h`a5yAeL?P+VFBmJOKz%io9nCEK~7HB;{yHz3cb_#92B8Lq50 z_yOx{KV8=s)i#tV$;gthzp4$?C%SV)LraXS=a|#9)1YG#jKQuediRD+C@T;~DZsR} zlIc`PN!2Q)R48L5FrA#K$LR7sM#m^R+(l#!zyF=cHTG>~ZpfT@m6wE4!mdm0C%P{6 zH14NTQz$e5lm^hVCf!w=Q}b_4A8f8V1bTRC2)#l?qm9 z7_u^DLaP;@b9dNQWrenIBQ;B@T%>$K`7;%H#`C2lDq}BmN)PMKV_)NB+d4a#zVh5B z?=wi^ACEOld%r)DU*PDL8&(NByke8~*8aX&pL&!{{NnTZ%D Date: Thu, 12 Jul 2018 17:18:45 -0400 Subject: [PATCH 21/43] [Beats Management] add more tests, update types, break out ES into it's own adapter (#20566) * inital effort to move to JWT and added jest based tests on libs * assign beats tests all passing * token tests now pass * add more tests * all tests now green * move enrollment token back to a hash * remove un-needed comment * alias lodash get to avoid confusion * isolated hash creation * Add initial efforts for backend framework adapter testing * move ES code to a DatabaseAdapter from BackendAdapter and add a TON of types for ES * re-typed * renamed types to match pattern * aditional renames * adapter tests should always just use adapterSetup(); * database now uses InternalRequest * corrected spelling of framework * fix typings * remove CRUFT * RequestOrInternal * Dont pass around request objects everywhere, just pass the user. Also, removed hapi types as they were not compatible * fix tests, add test, removed extra comment * fix auth * updated lock file --- x-pack/package.json | 2 - x-pack/plugins/beats/common/domain_types.ts | 31 ++ x-pack/plugins/beats/index.ts | 22 +- x-pack/plugins/beats/readme.md | 10 +- x-pack/plugins/beats/server/kibana.index.ts | 3 +- .../lib/adapters/beats/adapter_types.ts | 45 +++ .../beats/elasticsearch_beats_adapter.ts | 90 +++-- .../adapters/beats/memory_beats_adapter.ts | 41 ++- .../database/__tests__/kibana.test.ts | 34 ++ .../database/__tests__/test_contract.ts | 75 ++++ .../lib/adapters/database/adapter_types.ts | 327 ++++++++++++++++++ .../database/kibana_database_adapter.ts | 116 +++++++ .../framework/__tests__/kibana.test.ts | 36 ++ .../framework/__tests__/test_contract.ts | 36 ++ .../lib/adapters/framework/adapter_types.ts | 74 ++++ .../kibana_framework_adapter.ts | 59 ++-- .../testing_framework_adapter.ts | 27 +- .../server/lib/adapters/tags/adapter_types.ts | 12 + .../tags/elasticsearch_tags_adapter.ts | 25 +- .../lib/adapters/tags/memory_tags_adapter.ts | 8 +- .../lib/adapters/tokens/adapter_types.ts | 20 ++ .../tokens/elasticsearch_tokens_adapter.ts | 36 +- .../adapters/tokens/memory_tokens_adapter.ts | 16 +- .../beats/server/lib/compose/kibana.ts | 33 +- .../__tests__/beats/assign_tags.test.ts | 66 ++-- .../domains/__tests__/beats/enroll.test.ts | 28 +- .../domains/__tests__/beats/verify.test.ts | 25 +- .../lib/domains/__tests__/tokens.test.ts | 43 ++- .../plugins/beats/server/lib/domains/beats.ts | 55 ++- .../plugins/beats/server/lib/domains/tags.ts | 13 +- .../beats/server/lib/domains/tokens.ts | 10 +- x-pack/plugins/beats/server/lib/lib.ts | 198 +---------- .../plugins/beats/server/management_server.ts | 5 +- .../beats/server/rest_api/beats/enroll.ts | 3 +- .../beats/server/rest_api/beats/list.ts | 5 +- .../server/rest_api/beats/tag_assignment.ts | 5 +- .../server/rest_api/beats/tag_removal.ts | 5 +- .../beats/server/rest_api/beats/update.ts | 3 +- .../beats/server/rest_api/beats/verify.ts | 5 +- .../plugins/beats/server/rest_api/tags/set.ts | 5 +- .../beats/server/rest_api/tokens/create.ts | 6 +- ...es_error.test.js => wrap_es_error.test.ts} | 16 +- .../beats/server/utils/wrap_request.ts | 20 +- x-pack/plugins/beats/wallaby.js | 8 +- x-pack/yarn.lock | 6 - 45 files changed, 1161 insertions(+), 547 deletions(-) create mode 100644 x-pack/plugins/beats/common/domain_types.ts create mode 100644 x-pack/plugins/beats/server/lib/adapters/beats/adapter_types.ts create mode 100644 x-pack/plugins/beats/server/lib/adapters/database/__tests__/kibana.test.ts create mode 100644 x-pack/plugins/beats/server/lib/adapters/database/__tests__/test_contract.ts create mode 100644 x-pack/plugins/beats/server/lib/adapters/database/adapter_types.ts create mode 100644 x-pack/plugins/beats/server/lib/adapters/database/kibana_database_adapter.ts create mode 100644 x-pack/plugins/beats/server/lib/adapters/framework/__tests__/kibana.test.ts create mode 100644 x-pack/plugins/beats/server/lib/adapters/framework/__tests__/test_contract.ts create mode 100644 x-pack/plugins/beats/server/lib/adapters/framework/adapter_types.ts rename x-pack/plugins/beats/server/lib/adapters/{famework/kibana => framework}/kibana_framework_adapter.ts (56%) rename x-pack/plugins/beats/server/lib/adapters/{famework/kibana => framework}/testing_framework_adapter.ts (81%) create mode 100644 x-pack/plugins/beats/server/lib/adapters/tags/adapter_types.ts create mode 100644 x-pack/plugins/beats/server/lib/adapters/tokens/adapter_types.ts rename x-pack/plugins/beats/server/utils/error_wrappers/{wrap_es_error.test.js => wrap_es_error.test.ts} (73%) diff --git a/x-pack/package.json b/x-pack/package.json index 98267d8e33bb1..9cf203d0c6b83 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -29,8 +29,6 @@ "@kbn/test": "link:../packages/kbn-test", "@types/boom": "^4.3.8", "@types/chance": "^1.0.1", - "@types/expect.js": "^0.3.29", - "@types/hapi": "15.0.1", "@types/jest": "^22.2.3", "@types/joi": "^10.4.0", "@types/lodash": "^3.10.0", diff --git a/x-pack/plugins/beats/common/domain_types.ts b/x-pack/plugins/beats/common/domain_types.ts new file mode 100644 index 0000000000000..9411aca413840 --- /dev/null +++ b/x-pack/plugins/beats/common/domain_types.ts @@ -0,0 +1,31 @@ +/* + * 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 { ConfigurationBlockTypes } from './constants'; + +export interface ConfigurationBlock { + type: ConfigurationBlockTypes; + block_yml: string; +} + +export interface CMBeat { + id: string; + access_token: string; + verified_on?: string; + type: string; + version?: string; + host_ip: string; + host_name: string; + ephemeral_id?: string; + local_configuration_yml?: string; + tags?: string[]; + central_configuration_yml?: string; + metadata?: {}; +} + +export interface BeatTag { + id: string; + configuration_blocks: ConfigurationBlock[]; +} diff --git a/x-pack/plugins/beats/index.ts b/x-pack/plugins/beats/index.ts index ced89c186f73e..25be5728c93bb 100644 --- a/x-pack/plugins/beats/index.ts +++ b/x-pack/plugins/beats/index.ts @@ -10,18 +10,20 @@ import { initServerWithKibana } from './server/kibana.index'; const DEFAULT_ENROLLMENT_TOKENS_TTL_S = 10 * 60; // 10 minutes +export const config = Joi.object({ + enabled: Joi.boolean().default(true), + encryptionKey: Joi.string(), + enrollmentTokensTtlInSeconds: Joi.number() + .integer() + .min(1) + .default(DEFAULT_ENROLLMENT_TOKENS_TTL_S), +}).default(); +export const configPrefix = 'xpack.beats'; + export function beats(kibana: any) { return new kibana.Plugin({ - config: () => - Joi.object({ - enabled: Joi.boolean().default(true), - encryptionKey: Joi.string(), - enrollmentTokensTtlInSeconds: Joi.number() - .integer() - .min(1) - .default(DEFAULT_ENROLLMENT_TOKENS_TTL_S), - }).default(), - configPrefix: 'xpack.beats', + config: () => config, + configPrefix, id: PLUGIN.ID, require: ['kibana', 'elasticsearch', 'xpack_main'], init(server: any) { diff --git a/x-pack/plugins/beats/readme.md b/x-pack/plugins/beats/readme.md index fdd56a393e573..725fe587c5aa8 100644 --- a/x-pack/plugins/beats/readme.md +++ b/x-pack/plugins/beats/readme.md @@ -1,7 +1,15 @@ # Documentation for Beats CM in x-pack kibana -### Run tests +### Run tests (from x-pack dir) + +Functional tests ``` node scripts/jest.js plugins/beats --watch ``` + +Functional API tests + +``` +node scripts/functional_tests --config test/api_integration/config +``` diff --git a/x-pack/plugins/beats/server/kibana.index.ts b/x-pack/plugins/beats/server/kibana.index.ts index c9bc9b8bf02f4..dd7bc443bc603 100644 --- a/x-pack/plugins/beats/server/kibana.index.ts +++ b/x-pack/plugins/beats/server/kibana.index.ts @@ -4,11 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Server } from 'hapi'; import { compose } from './lib/compose/kibana'; import { initManagementServer } from './management_server'; -export const initServerWithKibana = (hapiServer: Server) => { +export const initServerWithKibana = (hapiServer: any) => { const libs = compose(hapiServer); initManagementServer(libs); }; diff --git a/x-pack/plugins/beats/server/lib/adapters/beats/adapter_types.ts b/x-pack/plugins/beats/server/lib/adapters/beats/adapter_types.ts new file mode 100644 index 0000000000000..8ce36456a703d --- /dev/null +++ b/x-pack/plugins/beats/server/lib/adapters/beats/adapter_types.ts @@ -0,0 +1,45 @@ +/* + * 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 { CMBeat } from '../../../../common/domain_types'; +import { FrameworkUser } from '../framework/adapter_types'; + +// FIXME: fix getBeatsWithIds return type +export interface CMBeatsAdapter { + insert(beat: CMBeat): Promise; + update(beat: CMBeat): Promise; + get(id: string): any; + getAll(user: FrameworkUser): any; + getWithIds(user: FrameworkUser, beatIds: string[]): any; + verifyBeats(user: FrameworkUser, beatIds: string[]): any; + removeTagsFromBeats( + user: FrameworkUser, + removals: BeatsTagAssignment[] + ): Promise; + assignTagsToBeats( + user: FrameworkUser, + assignments: BeatsTagAssignment[] + ): Promise; +} + +export interface BeatsTagAssignment { + beatId: string; + tag: string; + idxInRequest?: number; +} + +interface BeatsReturnedTagAssignment { + status: number | null; + result?: string; +} + +export interface CMAssignmentReturn { + assignments: BeatsReturnedTagAssignment[]; +} + +export interface BeatsRemovalReturn { + removals: BeatsReturnedTagAssignment[]; +} diff --git a/x-pack/plugins/beats/server/lib/adapters/beats/elasticsearch_beats_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/beats/elasticsearch_beats_adapter.ts index 76fbf956dafc9..e2321ac6739a8 100644 --- a/x-pack/plugins/beats/server/lib/adapters/beats/elasticsearch_beats_adapter.ts +++ b/x-pack/plugins/beats/server/lib/adapters/beats/elasticsearch_beats_adapter.ts @@ -7,18 +7,18 @@ import { flatten, get as _get, omit } from 'lodash'; import moment from 'moment'; import { INDEX_NAMES } from '../../../../common/constants'; -import { - BackendFrameworkAdapter, - CMBeat, - CMBeatsAdapter, - CMTagAssignment, - FrameworkRequest, -} from '../../lib'; +import { CMBeat } from '../../../../common/domain_types'; +import { DatabaseAdapter } from '../database/adapter_types'; +import { BackendFrameworkAdapter } from '../framework/adapter_types'; +import { FrameworkUser } from '../framework/adapter_types'; +import { BeatsTagAssignment, CMBeatsAdapter } from './adapter_types'; export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { + private database: DatabaseAdapter; private framework: BackendFrameworkAdapter; - constructor(framework: BackendFrameworkAdapter) { + constructor(database: DatabaseAdapter, framework: BackendFrameworkAdapter) { + this.database = database; this.framework = framework; } @@ -30,7 +30,10 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { type: '_doc', }; - const response = await this.framework.callWithInternalUser('get', params); + const response = await this.database.get( + this.framework.internalUser, + params + ); if (!response.found) { return null; } @@ -44,14 +47,13 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { type: 'beat', }; - const params = { + await this.database.create(this.framework.internalUser, { body, id: `beat:${beat.id}`, index: INDEX_NAMES.BEATS, refresh: 'wait_for', type: '_doc', - }; - await this.framework.callWithInternalUser('create', params); + }); } public async update(beat: CMBeat) { @@ -67,10 +69,10 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { refresh: 'wait_for', type: '_doc', }; - return await this.framework.callWithInternalUser('index', params); + await this.database.index(this.framework.internalUser, params); } - public async getWithIds(req: FrameworkRequest, beatIds: string[]) { + public async getWithIds(user: FrameworkUser, beatIds: string[]) { const ids = beatIds.map(beatId => `beat:${beatId}`); const params = { @@ -81,14 +83,14 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { index: INDEX_NAMES.BEATS, type: '_doc', }; - const response = await this.framework.callWithRequest(req, 'mget', params); + const response = await this.database.mget(user, params); - return get(response, 'docs', []) + return _get(response, 'docs', []) .filter((b: any) => b.found) .map((b: any) => b._source.beat); } - public async verifyBeats(req: FrameworkRequest, beatIds: string[]) { + public async verifyBeats(user: FrameworkUser, beatIds: string[]) { if (!Array.isArray(beatIds) || beatIds.length === 0) { return []; } @@ -101,15 +103,13 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { ]) ); - const params = { + const response = await this.database.bulk(user, { _sourceInclude: ['beat.id', 'beat.verified_on'], body, index: INDEX_NAMES.BEATS, refresh: 'wait_for', type: '_doc', - }; - - const response = await this.framework.callWithRequest(req, 'bulk', params); + }); return _get(response, 'items', []).map(b => ({ ..._get(b, 'update.get._source.beat', {}), @@ -117,26 +117,22 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { })); } - public async getAll(req: FrameworkRequest) { + public async getAll(user: FrameworkUser) { const params = { index: INDEX_NAMES.BEATS, q: 'type:beat', type: '_doc', }; - const response = await this.framework.callWithRequest( - req, - 'search', - params - ); + const response = await this.database.search(user, params); - const beats = get(response, 'hits.hits', []); + const beats = _get(response, 'hits.hits', []); return beats.map((beat: any) => omit(beat._source.beat, ['access_token'])); } public async removeTagsFromBeats( - req: FrameworkRequest, - removals: CMTagAssignment[] - ): Promise { + user: FrameworkUser, + removals: BeatsTagAssignment[] + ): Promise { const body = flatten( removals.map(({ beatId, tag }) => { const script = @@ -153,15 +149,13 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { }) ); - const params = { + const response = await this.database.bulk(user, { body, index: INDEX_NAMES.BEATS, refresh: 'wait_for', type: '_doc', - }; - - const response = await this.framework.callWithRequest(req, 'bulk', params); - return get(response, 'items', []).map( + }); + return _get(response, 'items', []).map( (item: any, resultIdx: number) => ({ idxInRequest: removals[resultIdx].idxInRequest, result: item.update.result, @@ -171,9 +165,9 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { } public async assignTagsToBeats( - req: FrameworkRequest, - assignments: CMTagAssignment[] - ): Promise { + user: FrameworkUser, + assignments: BeatsTagAssignment[] + ): Promise { const body = flatten( assignments.map(({ beatId, tag }) => { const script = @@ -193,18 +187,18 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { }) ); - const params = { + const response = await this.database.bulk(user, { body, index: INDEX_NAMES.BEATS, refresh: 'wait_for', type: '_doc', - }; - - const response = await this.framework.callWithRequest(req, 'bulk', params); - return get(response, 'items', []).map((item: any, resultIdx: any) => ({ - idxInRequest: assignments[resultIdx].idxInRequest, - result: item.update.result, - status: item.update.status, - })); + }); + return _get(response, 'items', []).map( + (item: any, resultIdx: any) => ({ + idxInRequest: assignments[resultIdx].idxInRequest, + result: item.update.result, + status: item.update.status, + }) + ); } } diff --git a/x-pack/plugins/beats/server/lib/adapters/beats/memory_beats_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/beats/memory_beats_adapter.ts index 9de8297c0f73e..a904b1d6831cd 100644 --- a/x-pack/plugins/beats/server/lib/adapters/beats/memory_beats_adapter.ts +++ b/x-pack/plugins/beats/server/lib/adapters/beats/memory_beats_adapter.ts @@ -7,12 +7,9 @@ import { omit } from 'lodash'; import moment from 'moment'; -import { - CMBeat, - CMBeatsAdapter, - CMTagAssignment, - FrameworkRequest, -} from '../../lib'; +import { CMBeat } from '../../../../common/domain_types'; +import { FrameworkUser } from '../framework/adapter_types'; +import { BeatsTagAssignment, CMBeatsAdapter } from './adapter_types'; export class MemoryBeatsAdapter implements CMBeatsAdapter { private beatsDB: CMBeat[]; @@ -38,11 +35,11 @@ export class MemoryBeatsAdapter implements CMBeatsAdapter { }; } - public async getWithIds(req: FrameworkRequest, beatIds: string[]) { + public async getWithIds(user: FrameworkUser, beatIds: string[]) { return this.beatsDB.filter(beat => beatIds.includes(beat.id)); } - public async verifyBeats(req: FrameworkRequest, beatIds: string[]) { + public async verifyBeats(user: FrameworkUser, beatIds: string[]) { if (!Array.isArray(beatIds) || beatIds.length === 0) { return []; } @@ -58,14 +55,14 @@ export class MemoryBeatsAdapter implements CMBeatsAdapter { return this.beatsDB.filter(beat => beatIds.includes(beat.id)); } - public async getAll(req: FrameworkRequest) { + public async getAll(user: FrameworkUser) { return this.beatsDB.map((beat: any) => omit(beat, ['access_token'])); } public async removeTagsFromBeats( - req: FrameworkRequest, - removals: CMTagAssignment[] - ): Promise { + user: FrameworkUser, + removals: BeatsTagAssignment[] + ): Promise { const beatIds = removals.map(r => r.beatId); const response = this.beatsDB @@ -88,16 +85,16 @@ export class MemoryBeatsAdapter implements CMBeatsAdapter { } public async assignTagsToBeats( - req: FrameworkRequest, - assignments: CMTagAssignment[] - ): Promise { + user: FrameworkUser, + assignments: BeatsTagAssignment[] + ): Promise { const beatIds = assignments.map(r => r.beatId); this.beatsDB.filter(beat => beatIds.includes(beat.id)).map(beat => { // get tags that need to be assigned to this beat const tags = assignments .filter(a => a.beatId === beat.id) - .map((t: CMTagAssignment) => t.tag); + .map((t: BeatsTagAssignment) => t.tag); if (tags.length > 0) { if (!beat.tags) { @@ -114,10 +111,12 @@ export class MemoryBeatsAdapter implements CMBeatsAdapter { return beat; }); - return assignments.map((item: CMTagAssignment, resultIdx: number) => ({ - idxInRequest: assignments[resultIdx].idxInRequest, - result: 'updated', - status: 200, - })); + return assignments.map( + (item: BeatsTagAssignment, resultIdx: number) => ({ + idxInRequest: assignments[resultIdx].idxInRequest, + result: 'updated', + status: 200, + }) + ); } } diff --git a/x-pack/plugins/beats/server/lib/adapters/database/__tests__/kibana.test.ts b/x-pack/plugins/beats/server/lib/adapters/database/__tests__/kibana.test.ts new file mode 100644 index 0000000000000..19bf05c3c777e --- /dev/null +++ b/x-pack/plugins/beats/server/lib/adapters/database/__tests__/kibana.test.ts @@ -0,0 +1,34 @@ +/* + * 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. + */ +// file.skip + +// @ts-ignore +import { createEsTestCluster } from '@kbn/test'; +// @ts-ignore +import * as kbnTestServer from '../../../../../../../../src/test_utils/kbn_server'; +import { DatabaseKbnESPlugin } from '../adapter_types'; +import { KibanaDatabaseAdapter } from '../kibana_database_adapter'; +import { contractTests } from './test_contract'; + +const kbnServer = kbnTestServer.createServerWithCorePlugins(); +const es = createEsTestCluster({}); + +contractTests('Kibana Database Adapter', { + before: async () => { + await es.start(); + await kbnServer.ready(); + + return await kbnServer.server.plugins.elasticsearch.waitUntilReady(); + }, + after: async () => { + await kbnServer.close(); + return await es.cleanup(); + }, + adapterSetup: () => { + return new KibanaDatabaseAdapter(kbnServer.server.plugins + .elasticsearch as DatabaseKbnESPlugin); + }, +}); diff --git a/x-pack/plugins/beats/server/lib/adapters/database/__tests__/test_contract.ts b/x-pack/plugins/beats/server/lib/adapters/database/__tests__/test_contract.ts new file mode 100644 index 0000000000000..ec30d4578e938 --- /dev/null +++ b/x-pack/plugins/beats/server/lib/adapters/database/__tests__/test_contract.ts @@ -0,0 +1,75 @@ +/* + * 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 { beatsIndexTemplate } from '../../../../utils/index_templates'; +import { DatabaseAdapter } from '../adapter_types'; + +interface ContractConfig { + before?(): Promise; + after?(): Promise; + adapterSetup(): DatabaseAdapter; +} + +export const contractTests = (testName: string, config: ContractConfig) => { + describe(testName, () => { + let database: DatabaseAdapter; + beforeAll(async () => { + jest.setTimeout(100000); // 1 second + + if (config.before) { + await config.before(); + } + }); + afterAll(async () => config.after && (await config.after())); + beforeEach(async () => { + database = config.adapterSetup(); + }); + + it('Should inject template into ES', async () => { + try { + await database.putTemplate( + { kind: 'internal' }, + { + name: 'beats-template', + body: beatsIndexTemplate, + } + ); + } catch (e) { + expect(e).toEqual(null); + } + }); + + it('Unauthorized users cant query', async () => { + const params = { + id: `beat:foo`, + ignore: [404], + index: '.management-beats', + type: '_doc', + }; + let ranWithoutError = false; + try { + await database.get({ kind: 'unauthenticated' }, params); + ranWithoutError = true; + } catch (e) { + expect(e).not.toEqual(null); + } + expect(ranWithoutError).toEqual(false); + }); + + it('Should query ES', async () => { + const params = { + id: `beat:foo`, + ignore: [404], + index: '.management-beats', + type: '_doc', + }; + const response = await database.get({ kind: 'internal' }, params); + + expect(response).not.toEqual(undefined); + // @ts-ignore + expect(response.found).toEqual(undefined); + }); + }); +}; diff --git a/x-pack/plugins/beats/server/lib/adapters/database/adapter_types.ts b/x-pack/plugins/beats/server/lib/adapters/database/adapter_types.ts new file mode 100644 index 0000000000000..36b5a35742bc9 --- /dev/null +++ b/x-pack/plugins/beats/server/lib/adapters/database/adapter_types.ts @@ -0,0 +1,327 @@ +/* + * 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 { FrameworkRequest, FrameworkUser } from '../framework/adapter_types'; +export interface DatabaseAdapter { + putTemplate( + user: FrameworkUser, + params: DatabasePutTemplateParams + ): Promise; + get( + user: FrameworkUser, + params: DatabaseGetParams + ): Promise>; + create( + user: FrameworkUser, + params: DatabaseCreateDocumentParams + ): Promise; + index( + user: FrameworkUser, + params: DatabaseIndexDocumentParams + ): Promise; + delete( + user: FrameworkUser, + params: DatabaseDeleteDocumentParams + ): Promise; + mget( + user: FrameworkUser, + params: DatabaseMGetParams + ): Promise>; + bulk( + user: FrameworkUser, + params: DatabaseBulkIndexDocumentsParams + ): Promise; + search( + user: FrameworkUser, + params: DatabaseSearchParams + ): Promise>; +} + +export interface DatabaseKbnESCluster { + callWithInternalUser(esMethod: string, options: {}): Promise; + callWithRequest( + req: FrameworkRequest, + esMethod: string, + options: {} + ): Promise; +} + +export interface DatabaseKbnESPlugin { + getCluster(clusterName: string): DatabaseKbnESCluster; +} + +export interface DatabaseSearchParams extends DatabaseGenericParams { + analyzer?: string; + analyzeWildcard?: boolean; + defaultOperator?: DefaultOperator; + df?: string; + explain?: boolean; + storedFields?: DatabaseNameList; + docvalueFields?: DatabaseNameList; + fielddataFields?: DatabaseNameList; + from?: number; + ignoreUnavailable?: boolean; + allowNoIndices?: boolean; + expandWildcards?: ExpandWildcards; + lenient?: boolean; + lowercaseExpandedTerms?: boolean; + preference?: string; + q?: string; + routing?: DatabaseNameList; + scroll?: string; + searchType?: 'query_then_fetch' | 'dfs_query_then_fetch'; + size?: number; + sort?: DatabaseNameList; + _source?: DatabaseNameList; + _sourceExclude?: DatabaseNameList; + _sourceInclude?: DatabaseNameList; + terminateAfter?: number; + stats?: DatabaseNameList; + suggestField?: string; + suggestMode?: 'missing' | 'popular' | 'always'; + suggestSize?: number; + suggestText?: string; + timeout?: string; + trackScores?: boolean; + version?: boolean; + requestCache?: boolean; + index?: DatabaseNameList; + type?: DatabaseNameList; +} + +export interface DatabaseSearchResponse { + took: number; + timed_out: boolean; + _scroll_id?: string; + _shards: DatabaseShardsResponse; + hits: { + total: number; + max_score: number; + hits: Array<{ + _index: string; + _type: string; + _id: string; + _score: number; + _source: T; + _version?: number; + _explanation?: DatabaseExplanation; + fields?: any; + highlight?: any; + inner_hits?: any; + sort?: string[]; + }>; + }; + aggregations?: any; +} + +export interface DatabaseExplanation { + value: number; + description: string; + details: DatabaseExplanation[]; +} + +export interface DatabaseShardsResponse { + total: number; + successful: number; + failed: number; + skipped: number; +} + +export interface DatabaseGetDocumentResponse { + _index: string; + _type: string; + _id: string; + _version: number; + found: boolean; + _source: Source; +} + +export interface DatabaseBulkResponse { + took: number; + errors: boolean; + items: Array< + | DatabaseDeleteDocumentResponse + | DatabaseIndexDocumentResponse + | DatabaseUpdateDocumentResponse + >; +} + +export interface DatabaseBulkIndexDocumentsParams + extends DatabaseGenericParams { + waitForActiveShards?: string; + refresh?: DatabaseRefresh; + routing?: string; + timeout?: string; + type?: string; + fields?: DatabaseNameList; + _source?: DatabaseNameList; + _sourceExclude?: DatabaseNameList; + _sourceInclude?: DatabaseNameList; + pipeline?: string; + index?: string; +} + +export interface DatabaseMGetParams extends DatabaseGenericParams { + storedFields?: DatabaseNameList; + preference?: string; + realtime?: boolean; + refresh?: boolean; + _source?: DatabaseNameList; + _sourceExclude?: DatabaseNameList; + _sourceInclude?: DatabaseNameList; + index: string; + type?: string; +} + +export interface DatabaseMGetResponse { + docs?: Array>; +} + +export interface DatabasePutTemplateParams extends DatabaseGenericParams { + name: string; + body: any; +} + +export interface DatabaseDeleteDocumentParams extends DatabaseGenericParams { + waitForActiveShards?: string; + parent?: string; + refresh?: DatabaseRefresh; + routing?: string; + timeout?: string; + version?: number; + versionType?: DatabaseVersionType; + index: string; + type: string; + id: string; +} + +export interface DatabaseIndexDocumentResponse { + found: boolean; + _index: string; + _type: string; + _id: string; + _version: number; + result: string; +} + +export interface DatabaseUpdateDocumentResponse { + found: boolean; + _index: string; + _type: string; + _id: string; + _version: number; + result: string; +} + +export interface DatabaseDeleteDocumentResponse { + found: boolean; + _index: string; + _type: string; + _id: string; + _version: number; + result: string; +} + +export interface DatabaseIndexDocumentParams extends DatabaseGenericParams { + waitForActiveShards?: string; + opType?: 'index' | 'create'; + parent?: string; + refresh?: string; + routing?: string; + timeout?: string; + timestamp?: Date | number; + ttl?: string; + version?: number; + versionType?: DatabaseVersionType; + pipeline?: string; + id?: string; + index: string; + type: string; + body: T; +} + +export interface DatabaseGetResponse { + found: boolean; + _source: T; +} +export interface DatabaseCreateDocumentParams extends DatabaseGenericParams { + waitForActiveShards?: string; + parent?: string; + refresh?: DatabaseRefresh; + routing?: string; + timeout?: string; + timestamp?: Date | number; + ttl?: string; + version?: number; + versionType?: DatabaseVersionType; + pipeline?: string; + id?: string; + index: string; + type: string; +} + +export interface DatabaseCreateDocumentResponse { + created: boolean; + result: string; +} + +export interface DatabaseDeleteDocumentParams extends DatabaseGenericParams { + waitForActiveShards?: string; + parent?: string; + refresh?: DatabaseRefresh; + routing?: string; + timeout?: string; + version?: number; + versionType?: DatabaseVersionType; + index: string; + type: string; + id: string; +} + +export interface DatabaseGetParams extends DatabaseGenericParams { + storedFields?: DatabaseNameList; + parent?: string; + preference?: string; + realtime?: boolean; + refresh?: boolean; + routing?: string; + _source?: DatabaseNameList; + _sourceExclude?: DatabaseNameList; + _sourceInclude?: DatabaseNameList; + version?: number; + versionType?: DatabaseVersionType; + id: string; + index: string; + type: string; +} + +export type DatabaseNameList = string | string[] | boolean; +export type DatabaseRefresh = boolean | 'true' | 'false' | 'wait_for' | ''; +export type DatabaseVersionType = + | 'internal' + | 'external' + | 'external_gte' + | 'force'; +export type ExpandWildcards = 'open' | 'closed' | 'none' | 'all'; +export type DefaultOperator = 'AND' | 'OR'; +export type DatabaseConflicts = 'abort' | 'proceed'; + +export interface DatabaseGenericParams { + requestTimeout?: number; + maxRetries?: number; + method?: string; + body?: any; + ignore?: number | number[]; + filterPath?: string | string[]; +} + +export interface DatabaseDeleteDocumentResponse { + found: boolean; + _index: string; + _type: string; + _id: string; + _version: number; + result: string; +} diff --git a/x-pack/plugins/beats/server/lib/adapters/database/kibana_database_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/database/kibana_database_adapter.ts new file mode 100644 index 0000000000000..6c4d96446abd4 --- /dev/null +++ b/x-pack/plugins/beats/server/lib/adapters/database/kibana_database_adapter.ts @@ -0,0 +1,116 @@ +/* + * 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 { internalAuthData } from '../../../utils/wrap_request'; +import { FrameworkUser } from '../framework/adapter_types'; +import { + DatabaseAdapter, + DatabaseBulkIndexDocumentsParams, + DatabaseCreateDocumentParams, + DatabaseCreateDocumentResponse, + DatabaseDeleteDocumentParams, + DatabaseDeleteDocumentResponse, + DatabaseGetDocumentResponse, + DatabaseGetParams, + DatabaseIndexDocumentParams, + DatabaseKbnESCluster, + DatabaseKbnESPlugin, + DatabaseMGetParams, + DatabaseMGetResponse, + DatabasePutTemplateParams, + DatabaseSearchParams, + DatabaseSearchResponse, +} from './adapter_types'; + +export class KibanaDatabaseAdapter implements DatabaseAdapter { + private es: DatabaseKbnESCluster; + + constructor(kbnElasticSearch: DatabaseKbnESPlugin) { + this.es = kbnElasticSearch.getCluster('admin'); + } + public async putTemplate( + user: FrameworkUser, + params: DatabasePutTemplateParams + ): Promise { + const callES = this.getCallType(user); + const result = await callES('indices.putTemplate', params); + return result; + } + + public async get( + user: FrameworkUser, + params: DatabaseGetParams + ): Promise> { + const callES = this.getCallType(user); + const result = await callES('get', params); + return result; + // todo + } + + public async mget( + user: FrameworkUser, + params: DatabaseMGetParams + ): Promise> { + const callES = this.getCallType(user); + const result = await callES('mget', params); + return result; + // todo + } + + public async bulk( + user: FrameworkUser, + params: DatabaseBulkIndexDocumentsParams + ): Promise { + const callES = this.getCallType(user); + const result = await callES('bulk', params); + return result; + } + + public async create( + user: FrameworkUser, + params: DatabaseCreateDocumentParams + ): Promise { + const callES = this.getCallType(user); + const result = await callES('create', params); + return result; + } + public async index( + user: FrameworkUser, + params: DatabaseIndexDocumentParams + ): Promise { + const callES = this.getCallType(user); + const result = await callES('index', params); + return result; + } + public async delete( + user: FrameworkUser, + params: DatabaseDeleteDocumentParams + ): Promise { + const callES = this.getCallType(user); + const result = await callES('delete', params); + return result; + } + + public async search( + user: FrameworkUser, + params: DatabaseSearchParams + ): Promise> { + const callES = this.getCallType(user); + const result = await callES('search', params); + return result; + } + + private getCallType(user: FrameworkUser): any { + if (user.kind === 'authenticated') { + return this.es.callWithRequest.bind(null, { + headers: user[internalAuthData], + }); + } else if (user.kind === 'internal') { + return this.es.callWithInternalUser; + } else { + throw new Error('Invalid user type'); + } + } +} diff --git a/x-pack/plugins/beats/server/lib/adapters/framework/__tests__/kibana.test.ts b/x-pack/plugins/beats/server/lib/adapters/framework/__tests__/kibana.test.ts new file mode 100644 index 0000000000000..c87a7374837e3 --- /dev/null +++ b/x-pack/plugins/beats/server/lib/adapters/framework/__tests__/kibana.test.ts @@ -0,0 +1,36 @@ +/* + * 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. + */ +// file.skip + +// @ts-ignore +import { createEsTestCluster } from '@kbn/test'; +// @ts-ignore +import * as kbnTestServer from '../../../../../../../../src/test_utils/kbn_server'; +import { + config as beatsPluginConfig, + configPrefix, +} from '../../../../../index'; +import { KibanaBackendFrameworkAdapter } from '../kibana_framework_adapter'; +import { contractTests } from './test_contract'; + +const kbnServer = kbnTestServer.createServerWithCorePlugins(); + +contractTests('Kibana Framework Adapter', { + before: async () => { + await kbnServer.ready(); + + const config = kbnServer.server.config(); + config.extendSchema(beatsPluginConfig, {}, configPrefix); + + config.set('xpack.beats.encryptionKey', 'foo'); + }, + after: async () => { + await kbnServer.close(); + }, + adapterSetup: () => { + return new KibanaBackendFrameworkAdapter(kbnServer.server); + }, +}); diff --git a/x-pack/plugins/beats/server/lib/adapters/framework/__tests__/test_contract.ts b/x-pack/plugins/beats/server/lib/adapters/framework/__tests__/test_contract.ts new file mode 100644 index 0000000000000..b17d424e1baf2 --- /dev/null +++ b/x-pack/plugins/beats/server/lib/adapters/framework/__tests__/test_contract.ts @@ -0,0 +1,36 @@ +/* + * 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 { BackendFrameworkAdapter } from '../adapter_types'; + +interface ContractConfig { + before?(): Promise; + after?(): Promise; + adapterSetup(): BackendFrameworkAdapter; +} + +export const contractTests = (testName: string, config: ContractConfig) => { + describe(testName, () => { + // let frameworkAdapter: BackendFrameworkAdapter; + beforeAll(async () => { + jest.setTimeout(100000); // 1 second + + if (config.before) { + await config.before(); + } + }); + afterAll(async () => config.after && (await config.after())); + beforeEach(async () => { + // FIXME: one of these always should exist, type ContractConfig as such + // frameworkAdapter = (config.adapterSetup + // ? config.adapterSetup() + // : config.adapter) as BackendFrameworkAdapter; + }); + + it('Should have tests here', () => { + expect(true).toEqual(true); + }); + }); +}; diff --git a/x-pack/plugins/beats/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/beats/server/lib/adapters/framework/adapter_types.ts new file mode 100644 index 0000000000000..4be3589a6f043 --- /dev/null +++ b/x-pack/plugins/beats/server/lib/adapters/framework/adapter_types.ts @@ -0,0 +1,74 @@ +/* + * 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 { internalAuthData } from '../../../utils/wrap_request'; +export interface BackendFrameworkAdapter { + internalUser: FrameworkInternalUser; + version: string; + getSetting(settingPath: string): any; + exposeStaticDir(urlPath: string, dir: string): void; + registerRoute( + route: FrameworkRouteOptions + ): void; +} + +export interface FrameworkAuthenticatedUser { + kind: 'authenticated'; + [internalAuthData]: AuthDataType; +} + +export interface FrameworkUnAuthenticatedUser { + kind: 'unauthenticated'; +} + +export interface FrameworkInternalUser { + kind: 'internal'; +} + +export type FrameworkUser = + | FrameworkAuthenticatedUser + | FrameworkUnAuthenticatedUser + | FrameworkInternalUser; + +export interface FrameworkRequest< + InternalRequest extends FrameworkWrappableRequest = FrameworkWrappableRequest +> { + user: FrameworkUser; + headers: InternalRequest['headers']; + info: InternalRequest['info']; + payload: InternalRequest['payload']; + params: InternalRequest['params']; + query: InternalRequest['query']; +} + +export interface FrameworkRouteOptions< + RouteRequest extends FrameworkWrappableRequest, + RouteResponse +> { + path: string; + method: string | string[]; + vhost?: string; + handler: FrameworkRouteHandler; + config?: {}; +} + +export type FrameworkRouteHandler< + RouteRequest extends FrameworkWrappableRequest, + RouteResponse +> = (request: FrameworkRequest, reply: any) => void; + +export interface FrameworkWrappableRequest< + Payload = any, + Params = any, + Query = any, + Headers = any, + Info = any +> { + headers: Headers; + info: Info; + payload: Payload; + params: Params; + query: Query; +} diff --git a/x-pack/plugins/beats/server/lib/adapters/famework/kibana/kibana_framework_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/framework/kibana_framework_adapter.ts similarity index 56% rename from x-pack/plugins/beats/server/lib/adapters/famework/kibana/kibana_framework_adapter.ts rename to x-pack/plugins/beats/server/lib/adapters/framework/kibana_framework_adapter.ts index a54997370ac5d..7113baf5c26e6 100644 --- a/x-pack/plugins/beats/server/lib/adapters/famework/kibana/kibana_framework_adapter.ts +++ b/x-pack/plugins/beats/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -4,29 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ +import { wrapRequest } from '../../../utils/wrap_request'; import { BackendFrameworkAdapter, - FrameworkRequest, + FrameworkInternalUser, FrameworkRouteOptions, - WrappableRequest, -} from '../../../lib'; - -import { IStrictReply, Request, Server } from 'hapi'; -import { - internalFrameworkRequest, - wrapRequest, -} from '../../../../utils/wrap_request'; + FrameworkWrappableRequest, +} from './adapter_types'; export class KibanaBackendFrameworkAdapter implements BackendFrameworkAdapter { + public readonly internalUser: FrameworkInternalUser = { + kind: 'internal', + }; public version: string; - private server: Server; + private server: any; private cryptoHash: string | null; - constructor(hapiServer: Server) { + constructor(hapiServer: any) { this.server = hapiServer; - this.version = hapiServer.plugins.kibana.status.plugin.version; + if (hapiServer.plugins.kibana) { + this.version = hapiServer.plugins.kibana.status.plugin.version; + } else { + this.version = 'unknown'; + } this.cryptoHash = null; - this.validateConfig(); } @@ -52,41 +53,21 @@ export class KibanaBackendFrameworkAdapter implements BackendFrameworkAdapter { }); } - public registerRoute( - route: FrameworkRouteOptions - ) { - const wrappedHandler = (request: any, reply: IStrictReply) => + public registerRoute< + RouteRequest extends FrameworkWrappableRequest, + RouteResponse + >(route: FrameworkRouteOptions) { + const wrappedHandler = (request: any, reply: any) => route.handler(wrapRequest(request), reply); this.server.route({ - config: route.config, handler: wrappedHandler, method: route.method, path: route.path, + config: route.config, }); } - public installIndexTemplate(name: string, template: {}) { - return this.callWithInternalUser('indices.putTemplate', { - body: template, - name, - }); - } - - public async callWithInternalUser(esMethod: string, options: {}) { - const { elasticsearch } = this.server.plugins; - const { callWithInternalUser } = elasticsearch.getCluster('admin'); - return await callWithInternalUser(esMethod, options); - } - - public async callWithRequest(req: FrameworkRequest, ...rest: any[]) { - const internalRequest = req[internalFrameworkRequest]; - const { elasticsearch } = internalRequest.server.plugins; - const { callWithRequest } = elasticsearch.getCluster('data'); - const fields = await callWithRequest(internalRequest, ...rest); - return fields; - } - private validateConfig() { // @ts-ignore const config = this.server.config(); diff --git a/x-pack/plugins/beats/server/lib/adapters/famework/kibana/testing_framework_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/framework/testing_framework_adapter.ts similarity index 81% rename from x-pack/plugins/beats/server/lib/adapters/famework/kibana/testing_framework_adapter.ts rename to x-pack/plugins/beats/server/lib/adapters/framework/testing_framework_adapter.ts index 9c928a05cfd5a..3bffe274fba8d 100644 --- a/x-pack/plugins/beats/server/lib/adapters/famework/kibana/testing_framework_adapter.ts +++ b/x-pack/plugins/beats/server/lib/adapters/framework/testing_framework_adapter.ts @@ -3,16 +3,16 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - import { Client } from 'elasticsearch'; -import { Request } from 'hapi'; import { get } from 'lodash'; +import { FrameworkInternalUser } from './adapter_types'; + import { BackendFrameworkAdapter, FrameworkRequest, FrameworkRouteOptions, - WrappableRequest, -} from '../../../lib'; + FrameworkWrappableRequest, +} from './adapter_types'; interface TestSettings { enrollmentTokensTtlInSeconds: number; @@ -20,6 +20,9 @@ interface TestSettings { } export class TestingBackendFrameworkAdapter implements BackendFrameworkAdapter { + public readonly internalUser: FrameworkInternalUser = { + kind: 'internal', + }; public version: string; private client: Client | null; private settings: TestSettings; @@ -46,19 +49,15 @@ export class TestingBackendFrameworkAdapter implements BackendFrameworkAdapter { // not yet testable } - public registerRoute( - route: FrameworkRouteOptions - ) { + public registerRoute< + RouteRequest extends FrameworkWrappableRequest, + RouteResponse + >(route: FrameworkRouteOptions) { // not yet testable } public installIndexTemplate(name: string, template: {}) { - if (this.client) { - return this.client.indices.putTemplate({ - body: template, - name, - }); - } + return; } public async callWithInternalUser(esMethod: string, options: {}) { @@ -70,7 +69,7 @@ export class TestingBackendFrameworkAdapter implements BackendFrameworkAdapter { } public async callWithRequest( - req: FrameworkRequest, + req: FrameworkRequest, esMethod: string, options: {} ) { diff --git a/x-pack/plugins/beats/server/lib/adapters/tags/adapter_types.ts b/x-pack/plugins/beats/server/lib/adapters/tags/adapter_types.ts new file mode 100644 index 0000000000000..19333c831d594 --- /dev/null +++ b/x-pack/plugins/beats/server/lib/adapters/tags/adapter_types.ts @@ -0,0 +1,12 @@ +/* + * 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 { BeatTag } from '../../../../common/domain_types'; +import { FrameworkUser } from '../framework/adapter_types'; + +export interface CMTagsAdapter { + getTagsWithIds(user: FrameworkUser, tagIds: string[]): any; + upsertTag(user: FrameworkUser, tag: BeatTag): Promise<{}>; +} diff --git a/x-pack/plugins/beats/server/lib/adapters/tags/elasticsearch_tags_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/tags/elasticsearch_tags_adapter.ts index 44aea344151ca..3f982f5edbb09 100644 --- a/x-pack/plugins/beats/server/lib/adapters/tags/elasticsearch_tags_adapter.ts +++ b/x-pack/plugins/beats/server/lib/adapters/tags/elasticsearch_tags_adapter.ts @@ -6,21 +6,20 @@ import { get } from 'lodash'; import { INDEX_NAMES } from '../../../../common/constants'; -import { - BackendFrameworkAdapter, - BeatTag, - CMTagsAdapter, - FrameworkRequest, -} from '../../lib'; +import { FrameworkUser } from './../framework/adapter_types'; + +import { BeatTag } from '../../../../common/domain_types'; +import { DatabaseAdapter } from '../database/adapter_types'; +import { CMTagsAdapter } from './adapter_types'; export class ElasticsearchTagsAdapter implements CMTagsAdapter { - private framework: BackendFrameworkAdapter; + private database: DatabaseAdapter; - constructor(framework: BackendFrameworkAdapter) { - this.framework = framework; + constructor(database: DatabaseAdapter) { + this.database = database; } - public async getTagsWithIds(req: FrameworkRequest, tagIds: string[]) { + public async getTagsWithIds(user: FrameworkUser, tagIds: string[]) { const ids = tagIds.map(tag => `tag:${tag}`); // TODO abstract to kibana adapter as the more generic getDocs @@ -32,7 +31,7 @@ export class ElasticsearchTagsAdapter implements CMTagsAdapter { index: INDEX_NAMES.BEATS, type: '_doc', }; - const response = await this.framework.callWithRequest(req, 'mget', params); + const response = await this.database.mget(user, params); return get(response, 'docs', []) .filter((b: any) => b.found) @@ -42,7 +41,7 @@ export class ElasticsearchTagsAdapter implements CMTagsAdapter { })); } - public async upsertTag(req: FrameworkRequest, tag: BeatTag) { + public async upsertTag(user: FrameworkUser, tag: BeatTag) { const body = { tag, type: 'tag', @@ -55,7 +54,7 @@ export class ElasticsearchTagsAdapter implements CMTagsAdapter { refresh: 'wait_for', type: '_doc', }; - const response = await this.framework.callWithRequest(req, 'index', params); + const response = await this.database.index(user, params); // TODO this is not something that works for TS... change this return type return get(response, 'result'); diff --git a/x-pack/plugins/beats/server/lib/adapters/tags/memory_tags_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/tags/memory_tags_adapter.ts index 4d2a80e1b39c2..64ef401008ae1 100644 --- a/x-pack/plugins/beats/server/lib/adapters/tags/memory_tags_adapter.ts +++ b/x-pack/plugins/beats/server/lib/adapters/tags/memory_tags_adapter.ts @@ -4,7 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { BeatTag, CMTagsAdapter, FrameworkRequest } from '../../lib'; +import { BeatTag } from '../../../../common/domain_types'; +import { FrameworkUser } from './../framework/adapter_types'; +import { CMTagsAdapter } from './adapter_types'; export class MemoryTagsAdapter implements CMTagsAdapter { private tagsDB: BeatTag[] = []; @@ -13,11 +15,11 @@ export class MemoryTagsAdapter implements CMTagsAdapter { this.tagsDB = tagsDB; } - public async getTagsWithIds(req: FrameworkRequest, tagIds: string[]) { + public async getTagsWithIds(user: FrameworkUser, tagIds: string[]) { return this.tagsDB.filter(tag => tagIds.includes(tag.id)); } - public async upsertTag(req: FrameworkRequest, tag: BeatTag) { + public async upsertTag(user: FrameworkUser, tag: BeatTag) { const existingTagIndex = this.tagsDB.findIndex(t => t.id === tag.id); if (existingTagIndex !== -1) { this.tagsDB[existingTagIndex] = tag; diff --git a/x-pack/plugins/beats/server/lib/adapters/tokens/adapter_types.ts b/x-pack/plugins/beats/server/lib/adapters/tokens/adapter_types.ts new file mode 100644 index 0000000000000..71fb719d9952a --- /dev/null +++ b/x-pack/plugins/beats/server/lib/adapters/tokens/adapter_types.ts @@ -0,0 +1,20 @@ +/* + * 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 { FrameworkUser } from '../framework/adapter_types'; + +export interface TokenEnrollmentData { + token: string | null; + expires_on: string; +} + +export interface CMTokensAdapter { + deleteEnrollmentToken(enrollmentToken: string): Promise; + getEnrollmentToken(enrollmentToken: string): Promise; + upsertTokens( + user: FrameworkUser, + tokens: TokenEnrollmentData[] + ): Promise; +} diff --git a/x-pack/plugins/beats/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts index 7a63c784ecf6a..6dbefce514052 100644 --- a/x-pack/plugins/beats/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts +++ b/x-pack/plugins/beats/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts @@ -6,17 +6,19 @@ import { flatten, get } from 'lodash'; import { INDEX_NAMES } from '../../../../common/constants'; +import { DatabaseAdapter } from '../database/adapter_types'; import { BackendFrameworkAdapter, - CMTokensAdapter, - EnrollmentToken, - FrameworkRequest, -} from '../../lib'; + FrameworkUser, +} from '../framework/adapter_types'; +import { CMTokensAdapter, TokenEnrollmentData } from './adapter_types'; export class ElasticsearchTokensAdapter implements CMTokensAdapter { + private database: DatabaseAdapter; private framework: BackendFrameworkAdapter; - constructor(framework: BackendFrameworkAdapter) { + constructor(database: DatabaseAdapter, framework: BackendFrameworkAdapter) { + this.database = database; this.framework = framework; } @@ -27,12 +29,12 @@ export class ElasticsearchTokensAdapter implements CMTokensAdapter { type: '_doc', }; - return this.framework.callWithInternalUser('delete', params); + await this.database.delete(this.framework.internalUser, params); } public async getEnrollmentToken( tokenString: string - ): Promise { + ): Promise { const params = { id: `enrollment_token:${tokenString}`, ignore: [404], @@ -40,8 +42,11 @@ export class ElasticsearchTokensAdapter implements CMTokensAdapter { type: '_doc', }; - const response = await this.framework.callWithInternalUser('get', params); - const tokenDetails = get( + const response = await this.database.get( + this.framework.internalUser, + params + ); + const tokenDetails = get( response, '_source.enrollment_token', { @@ -55,12 +60,15 @@ export class ElasticsearchTokensAdapter implements CMTokensAdapter { // out whether a token is valid or not. So we introduce a random delay in returning from // this function to obscure the actual time it took for Elasticsearch to find the token. const randomDelayInMs = 25 + Math.round(Math.random() * 200); // between 25 and 225 ms - return new Promise(resolve => + return new Promise(resolve => setTimeout(() => resolve(tokenDetails), randomDelayInMs) ); } - public async upsertTokens(req: FrameworkRequest, tokens: EnrollmentToken[]) { + public async upsertTokens( + user: FrameworkUser, + tokens: TokenEnrollmentData[] + ) { const body = flatten( tokens.map(token => [ { index: { _id: `enrollment_token:${token.token}` } }, @@ -71,14 +79,12 @@ export class ElasticsearchTokensAdapter implements CMTokensAdapter { ]) ); - const params = { + await this.database.bulk(user, { body, index: INDEX_NAMES.BEATS, refresh: 'wait_for', type: '_doc', - }; - - await this.framework.callWithRequest(req, 'bulk', params); + }); return tokens; } } diff --git a/x-pack/plugins/beats/server/lib/adapters/tokens/memory_tokens_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/tokens/memory_tokens_adapter.ts index 1734327007e08..1eed188d3f2e9 100644 --- a/x-pack/plugins/beats/server/lib/adapters/tokens/memory_tokens_adapter.ts +++ b/x-pack/plugins/beats/server/lib/adapters/tokens/memory_tokens_adapter.ts @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CMTokensAdapter, EnrollmentToken, FrameworkRequest } from '../../lib'; +import { FrameworkAuthenticatedUser } from './../framework/adapter_types'; +import { CMTokensAdapter, TokenEnrollmentData } from './adapter_types'; export class MemoryTokensAdapter implements CMTokensAdapter { - private tokenDB: EnrollmentToken[]; + private tokenDB: TokenEnrollmentData[]; - constructor(tokenDB: EnrollmentToken[]) { + constructor(tokenDB: TokenEnrollmentData[]) { this.tokenDB = tokenDB; } @@ -25,13 +26,16 @@ export class MemoryTokensAdapter implements CMTokensAdapter { public async getEnrollmentToken( tokenString: string - ): Promise { - return new Promise(resolve => { + ): Promise { + return new Promise(resolve => { return resolve(this.tokenDB.find(token => token.token === tokenString)); }); } - public async upsertTokens(req: FrameworkRequest, tokens: EnrollmentToken[]) { + public async upsertTokens( + user: FrameworkAuthenticatedUser, + tokens: TokenEnrollmentData[] + ) { tokens.forEach(token => { const existingIndex = this.tokenDB.findIndex( t => t.token === token.token diff --git a/x-pack/plugins/beats/server/lib/compose/kibana.ts b/x-pack/plugins/beats/server/lib/compose/kibana.ts index ff478646aea89..7c46f82b84bf3 100644 --- a/x-pack/plugins/beats/server/lib/compose/kibana.ts +++ b/x-pack/plugins/beats/server/lib/compose/kibana.ts @@ -7,8 +7,9 @@ import { ElasticsearchBeatsAdapter } from '../adapters/beats/elasticsearch_beats_adapter'; import { ElasticsearchTagsAdapter } from '../adapters/tags/elasticsearch_tags_adapter'; import { ElasticsearchTokensAdapter } from '../adapters/tokens/elasticsearch_tokens_adapter'; +import { KibanaDatabaseAdapter } from './../adapters/database/kibana_database_adapter'; -import { KibanaBackendFrameworkAdapter } from '../adapters/famework/kibana/kibana_framework_adapter'; +import { KibanaBackendFrameworkAdapter } from '../adapters/framework/kibana_framework_adapter'; import { CMBeatsDomain } from '../domains/beats'; import { CMTagsDomain } from '../domains/tags'; @@ -16,19 +17,24 @@ import { CMTokensDomain } from '../domains/tokens'; import { CMDomainLibs, CMServerLibs } from '../lib'; -import { Server } from 'hapi'; - -export function compose(server: Server): CMServerLibs { +export function compose(server: any): CMServerLibs { const framework = new KibanaBackendFrameworkAdapter(server); - - const tags = new CMTagsDomain(new ElasticsearchTagsAdapter(framework)); - const tokens = new CMTokensDomain(new ElasticsearchTokensAdapter(framework), { - framework, - }); - const beats = new CMBeatsDomain(new ElasticsearchBeatsAdapter(framework), { - tags, - tokens, - }); + const database = new KibanaDatabaseAdapter(server.plugins.elasticsearch); + + const tags = new CMTagsDomain(new ElasticsearchTagsAdapter(database)); + const tokens = new CMTokensDomain( + new ElasticsearchTokensAdapter(database, framework), + { + framework, + } + ); + const beats = new CMBeatsDomain( + new ElasticsearchBeatsAdapter(database, framework), + { + tags, + tokens, + } + ); const domainLibs: CMDomainLibs = { beats, @@ -38,6 +44,7 @@ export function compose(server: Server): CMServerLibs { const libs: CMServerLibs = { framework, + database, ...domainLibs, }; diff --git a/x-pack/plugins/beats/server/lib/domains/__tests__/beats/assign_tags.test.ts b/x-pack/plugins/beats/server/lib/domains/__tests__/beats/assign_tags.test.ts index c1e360ffd75f4..f28fcda8004e4 100644 --- a/x-pack/plugins/beats/server/lib/domains/__tests__/beats/assign_tags.test.ts +++ b/x-pack/plugins/beats/server/lib/domains/__tests__/beats/assign_tags.test.ts @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from 'expect.js'; -import { wrapRequest } from '../../../../utils/wrap_request'; +import { FrameworkInternalUser } from './../../../adapters/framework/adapter_types'; + import { MemoryBeatsAdapter } from '../../../adapters/beats/memory_beats_adapter'; -import { TestingBackendFrameworkAdapter } from '../../../adapters/famework/kibana/testing_framework_adapter'; +import { TestingBackendFrameworkAdapter } from '../../../adapters/framework/testing_framework_adapter'; import { MemoryTagsAdapter } from '../../../adapters/tags/memory_tags_adapter'; import { MemoryTokensAdapter } from '../../../adapters/tokens/memory_tokens_adapter'; -import { BeatTag, CMBeat } from './../../../lib'; +import { BeatTag, CMBeat } from '../../../../../common/domain_types'; import { CMBeatsDomain } from '../../beats'; import { CMTagsDomain } from '../../tags'; @@ -22,13 +22,7 @@ import Chance from 'chance'; const seed = Date.now(); const chance = new Chance(seed); -const fakeReq = wrapRequest({ - headers: {}, - info: {}, - params: {}, - payload: {}, - query: {}, -}); +const internalUser: FrameworkInternalUser = { kind: 'internal' }; const settings = { encryptionKey: 'something_who_cares', @@ -103,11 +97,11 @@ describe('Beats Domain Lib', () => { }); it('should add a single tag to a single beat', async () => { - const apiResponse = await beatsLib.assignTagsToBeats(fakeReq, [ + const apiResponse = await beatsLib.assignTagsToBeats(internalUser, [ { beatId: 'bar', tag: 'production' }, ]); - expect(apiResponse.assignments).to.eql([ + expect(apiResponse.assignments).toEqual([ { status: 200, result: 'updated' }, ]); }); @@ -116,80 +110,80 @@ describe('Beats Domain Lib', () => { const tags = ['production']; let beat = beatsDB.find(b => b.id === 'foo') as any; - expect(beat.tags).to.eql([...tags, 'qa']); + expect(beat.tags).toEqual([...tags, 'qa']); // Adding the existing tag - const apiResponse = await beatsLib.assignTagsToBeats(fakeReq, [ + const apiResponse = await beatsLib.assignTagsToBeats(internalUser, [ { beatId: 'foo', tag: 'production' }, ]); - expect(apiResponse.assignments).to.eql([ + expect(apiResponse.assignments).toEqual([ { status: 200, result: 'updated' }, ]); beat = beatsDB.find(b => b.id === 'foo') as any; - expect(beat.tags).to.eql([...tags, 'qa']); + expect(beat.tags).toEqual([...tags, 'qa']); }); it('should add a single tag to a multiple beats', async () => { - const apiResponse = await beatsLib.assignTagsToBeats(fakeReq, [ + const apiResponse = await beatsLib.assignTagsToBeats(internalUser, [ { beatId: 'foo', tag: 'development' }, { beatId: 'bar', tag: 'development' }, ]); - expect(apiResponse.assignments).to.eql([ + expect(apiResponse.assignments).toEqual([ { status: 200, result: 'updated' }, { status: 200, result: 'updated' }, ]); let beat = beatsDB.find(b => b.id === 'foo') as any; - expect(beat.tags).to.eql(['production', 'qa', 'development']); // as beat 'foo' already had 'production' and 'qa' tags attached to it + expect(beat.tags).toEqual(['production', 'qa', 'development']); // as beat 'foo' already had 'production' and 'qa' tags attached to it beat = beatsDB.find(b => b.id === 'bar') as any; - expect(beat.tags).to.eql(['development']); + expect(beat.tags).toEqual(['development']); }); it('should add multiple tags to a single beat', async () => { - const apiResponse = await beatsLib.assignTagsToBeats(fakeReq, [ + const apiResponse = await beatsLib.assignTagsToBeats(internalUser, [ { beatId: 'bar', tag: 'development' }, { beatId: 'bar', tag: 'production' }, ]); - expect(apiResponse.assignments).to.eql([ + expect(apiResponse.assignments).toEqual([ { status: 200, result: 'updated' }, { status: 200, result: 'updated' }, ]); const beat = beatsDB.find(b => b.id === 'bar') as any; - expect(beat.tags).to.eql(['development', 'production']); + expect(beat.tags).toEqual(['development', 'production']); }); it('should add multiple tags to a multiple beats', async () => { - const apiResponse = await beatsLib.assignTagsToBeats(fakeReq, [ + const apiResponse = await beatsLib.assignTagsToBeats(internalUser, [ { beatId: 'foo', tag: 'development' }, { beatId: 'bar', tag: 'production' }, ]); - expect(apiResponse.assignments).to.eql([ + expect(apiResponse.assignments).toEqual([ { status: 200, result: 'updated' }, { status: 200, result: 'updated' }, ]); let beat = beatsDB.find(b => b.id === 'foo') as any; - expect(beat.tags).to.eql(['production', 'qa', 'development']); // as beat 'foo' already had 'production' and 'qa' tags attached to it + expect(beat.tags).toEqual(['production', 'qa', 'development']); // as beat 'foo' already had 'production' and 'qa' tags attached to it beat = beatsDB.find(b => b.id === 'bar') as any; - expect(beat.tags).to.eql(['production']); + expect(beat.tags).toEqual(['production']); }); it('should return errors for non-existent beats', async () => { const nonExistentBeatId = chance.word(); - const apiResponse = await beatsLib.assignTagsToBeats(fakeReq, [ + const apiResponse = await beatsLib.assignTagsToBeats(internalUser, [ { beatId: nonExistentBeatId, tag: 'production' }, ]); - expect(apiResponse.assignments).to.eql([ + expect(apiResponse.assignments).toEqual([ { status: 404, result: `Beat ${nonExistentBeatId} not found` }, ]); }); @@ -197,27 +191,27 @@ describe('Beats Domain Lib', () => { it('should return errors for non-existent tags', async () => { const nonExistentTag = chance.word(); - const apiResponse = await beatsLib.assignTagsToBeats(fakeReq, [ + const apiResponse = await beatsLib.assignTagsToBeats(internalUser, [ { beatId: 'bar', tag: nonExistentTag }, ]); - expect(apiResponse.assignments).to.eql([ + expect(apiResponse.assignments).toEqual([ { status: 404, result: `Tag ${nonExistentTag} not found` }, ]); const beat = beatsDB.find(b => b.id === 'bar') as any; - expect(beat).to.not.have.property('tags'); + expect(beat).not.toHaveProperty('tags'); }); it('should return errors for non-existent beats and tags', async () => { const nonExistentBeatId = chance.word(); const nonExistentTag = chance.word(); - const apiResponse = await beatsLib.assignTagsToBeats(fakeReq, [ + const apiResponse = await beatsLib.assignTagsToBeats(internalUser, [ { beatId: nonExistentBeatId, tag: nonExistentTag }, ]); - expect(apiResponse.assignments).to.eql([ + expect(apiResponse.assignments).toEqual([ { result: `Beat ${nonExistentBeatId} and tag ${nonExistentTag} not found`, status: 404, @@ -225,7 +219,7 @@ describe('Beats Domain Lib', () => { ]); const beat = beatsDB.find(b => b.id === 'bar') as any; - expect(beat).to.not.have.property('tags'); + expect(beat).not.toHaveProperty('tags'); }); }); }); diff --git a/x-pack/plugins/beats/server/lib/domains/__tests__/beats/enroll.test.ts b/x-pack/plugins/beats/server/lib/domains/__tests__/beats/enroll.test.ts index f52f1096227c0..1466e9ed926c7 100644 --- a/x-pack/plugins/beats/server/lib/domains/__tests__/beats/enroll.test.ts +++ b/x-pack/plugins/beats/server/lib/domains/__tests__/beats/enroll.test.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from 'expect.js'; import { MemoryBeatsAdapter } from '../../../adapters/beats/memory_beats_adapter'; -import { TestingBackendFrameworkAdapter } from '../../../adapters/famework/kibana/testing_framework_adapter'; +import { TestingBackendFrameworkAdapter } from '../../../adapters/framework/testing_framework_adapter'; import { MemoryTagsAdapter } from '../../../adapters/tags/memory_tags_adapter'; import { MemoryTokensAdapter } from '../../../adapters/tokens/memory_tokens_adapter'; -import { BeatTag, CMBeat, EnrollmentToken } from './../../../lib'; +import { BeatTag, CMBeat } from '../../../../../common/domain_types'; +import { TokenEnrollmentData } from '../../../adapters/tokens/adapter_types'; import { CMBeatsDomain } from '../../beats'; import { CMTagsDomain } from '../../tags'; @@ -35,7 +35,7 @@ describe('Beats Domain Lib', () => { let beatsDB: CMBeat[] = []; let tagsDB: BeatTag[] = []; - let tokensDB: EnrollmentToken[] = []; + let tokensDB: TokenEnrollmentData[] = []; let validEnrollmentToken: string; let beatId: string; let beat: Partial; @@ -88,27 +88,27 @@ describe('Beats Domain Lib', () => { validEnrollmentToken ); - expect(token).to.equal(validEnrollmentToken); + expect(token).toEqual(validEnrollmentToken); const { accessToken } = await beatsLib.enrollBeat( beatId, '192.168.1.1', omit(beat, 'enrollment_token') ); - expect(beatsDB.length).to.eql(1); - expect(beatsDB[0]).to.have.property('host_ip'); + expect(beatsDB.length).toEqual(1); + expect(beatsDB[0]).toHaveProperty('host_ip'); - expect(accessToken).to.eql(beatsDB[0].access_token); + expect(accessToken).toEqual(beatsDB[0].access_token); await tokensLib.deleteEnrollmentToken(validEnrollmentToken); - expect(tokensDB.length).to.eql(0); + expect(tokensDB.length).toEqual(0); }); it('should reject an invalid enrollment token', async () => { const { token } = await tokensLib.getEnrollmentToken(chance.word()); - expect(token).to.eql(null); + expect(token).toEqual(null); }); it('should reject an expired enrollment token', async () => { @@ -118,19 +118,19 @@ describe('Beats Domain Lib', () => { }) ); - expect(token).to.eql(null); + expect(token).toEqual(null); }); it('should delete the given enrollment token so it may not be reused', async () => { - expect(tokensDB[0].token).to.eql(validEnrollmentToken); + expect(tokensDB[0].token).toEqual(validEnrollmentToken); await tokensLib.deleteEnrollmentToken(validEnrollmentToken); - expect(tokensDB.length).to.eql(0); + expect(tokensDB.length).toEqual(0); const { token } = await tokensLib.getEnrollmentToken( validEnrollmentToken ); - expect(token).to.eql(null); + expect(token).toEqual(null); }); }); }); diff --git a/x-pack/plugins/beats/server/lib/domains/__tests__/beats/verify.test.ts b/x-pack/plugins/beats/server/lib/domains/__tests__/beats/verify.test.ts index 7310b101a351a..140a17917684d 100644 --- a/x-pack/plugins/beats/server/lib/domains/__tests__/beats/verify.test.ts +++ b/x-pack/plugins/beats/server/lib/domains/__tests__/beats/verify.test.ts @@ -4,14 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from 'expect.js'; -import { wrapRequest } from '../../../../utils/wrap_request'; import { MemoryBeatsAdapter } from '../../../adapters/beats/memory_beats_adapter'; -import { TestingBackendFrameworkAdapter } from '../../../adapters/famework/kibana/testing_framework_adapter'; +import { TestingBackendFrameworkAdapter } from '../../../adapters/framework/testing_framework_adapter'; import { MemoryTagsAdapter } from '../../../adapters/tags/memory_tags_adapter'; import { MemoryTokensAdapter } from '../../../adapters/tokens/memory_tokens_adapter'; -import { BeatTag, CMBeat, EnrollmentToken } from './../../../lib'; +import { BeatTag, CMBeat } from '../../../../../common/domain_types'; +import { TokenEnrollmentData } from '../../../adapters/tokens/adapter_types'; import { CMBeatsDomain } from '../../beats'; import { CMTagsDomain } from '../../tags'; @@ -27,21 +26,13 @@ const settings = { enrollmentTokensTtlInSeconds: 10 * 60, // 10 minutes }; -const fakeReq = wrapRequest({ - headers: {}, - info: {}, - params: {}, - payload: {}, - query: {}, -}); - describe('Beats Domain Lib', () => { let beatsLib: CMBeatsDomain; let tokensLib: CMTokensDomain; let beatsDB: CMBeat[] = []; let tagsDB: BeatTag[] = []; - let tokensDB: EnrollmentToken[] = []; + let tokensDB: TokenEnrollmentData[] = []; describe('verify_beat', () => { beforeEach(async () => { @@ -123,7 +114,7 @@ describe('Beats Domain Lib', () => { verifiedBeatIds, alreadyVerifiedBeatIds, nonExistentBeatIds, - } = await beatsLib.verifyBeats(fakeReq, beatIds); + } = await beatsLib.verifyBeats({ kind: 'unauthenticated' }, beatIds); // TODO calculation of status should be done in-lib, w/switch statement here beats.forEach(b => { @@ -143,7 +134,7 @@ describe('Beats Domain Lib', () => { }); const response = { beats }; - expect(response.beats).to.eql([ + expect(response.beats).toEqual([ { id: 'bar', status: 200, result: 'verified' }, { id: nonExistentBeatId, status: 404, result: 'not found' }, ]); @@ -163,7 +154,7 @@ describe('Beats Domain Lib', () => { verifiedBeatIds, alreadyVerifiedBeatIds, nonExistentBeatIds, - } = await beatsLib.verifyBeats(fakeReq, beatIds); + } = await beatsLib.verifyBeats({ kind: 'unauthenticated' }, beatIds); // TODO calculation of status should be done in-lib, w/switch statement here beats.forEach(beat => { @@ -183,7 +174,7 @@ describe('Beats Domain Lib', () => { }); const response = { beats }; - expect(response.beats).to.eql([ + expect(response.beats).toEqual([ { id: 'foo', status: 200, result: 'already verified' }, { id: 'bar', status: 200, result: 'verified' }, ]); diff --git a/x-pack/plugins/beats/server/lib/domains/__tests__/tokens.test.ts b/x-pack/plugins/beats/server/lib/domains/__tests__/tokens.test.ts index 174c7d628778c..8002013fa4795 100644 --- a/x-pack/plugins/beats/server/lib/domains/__tests__/tokens.test.ts +++ b/x-pack/plugins/beats/server/lib/domains/__tests__/tokens.test.ts @@ -4,27 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from 'expect.js'; -import { wrapRequest } from '../../../utils/wrap_request'; -import { TestingBackendFrameworkAdapter } from '../../adapters/famework/kibana/testing_framework_adapter'; +import { TestingBackendFrameworkAdapter } from '../../adapters/framework/testing_framework_adapter'; +import { TokenEnrollmentData } from '../../adapters/tokens/adapter_types'; import { MemoryTokensAdapter } from '../../adapters/tokens/memory_tokens_adapter'; -import { EnrollmentToken } from '../../lib'; import { CMTokensDomain } from '../tokens'; import Chance from 'chance'; import moment from 'moment'; +import { BackendFrameworkAdapter } from '../../adapters/framework/adapter_types'; const seed = Date.now(); const chance = new Chance(seed); -const fakeReq = wrapRequest({ - headers: {}, - info: {}, - params: {}, - payload: {}, - query: {}, -}); - const settings = { encryptionKey: 'something_who_cares', enrollmentTokensTtlInSeconds: 10 * 60, // 10 minutes @@ -32,11 +23,12 @@ const settings = { describe('Token Domain Lib', () => { let tokensLib: CMTokensDomain; - let tokensDB: EnrollmentToken[] = []; + let tokensDB: TokenEnrollmentData[] = []; + let framework: BackendFrameworkAdapter; beforeEach(async () => { tokensDB = []; - const framework = new TestingBackendFrameworkAdapter(null, settings); + framework = new TestingBackendFrameworkAdapter(null, settings); tokensLib = new CMTokensDomain(new MemoryTokensAdapter(tokensDB), { framework, @@ -44,26 +36,31 @@ describe('Token Domain Lib', () => { }); it('should generate webtokens with a qty of 1', async () => { - const tokens = await tokensLib.createEnrollmentTokens(fakeReq, 1); + const tokens = await tokensLib.createEnrollmentTokens( + framework.internalUser, + 1 + ); - expect(tokens.length).to.be(1); + expect(tokens.length).toBe(1); - expect(typeof tokens[0]).to.be('string'); + expect(typeof tokens[0]).toBe('string'); }); it('should create the specified number of tokens', async () => { const numTokens = chance.integer({ min: 1, max: 20 }); const tokensFromApi = await tokensLib.createEnrollmentTokens( - fakeReq, + framework.internalUser, numTokens ); - expect(tokensFromApi.length).to.eql(numTokens); - expect(tokensFromApi).to.eql(tokensDB.map((t: EnrollmentToken) => t.token)); + expect(tokensFromApi.length).toEqual(numTokens); + expect(tokensFromApi).toEqual( + tokensDB.map((t: TokenEnrollmentData) => t.token) + ); }); it('should set token expiration to 10 minutes from now by default', async () => { - await tokensLib.createEnrollmentTokens(fakeReq, 1); + await tokensLib.createEnrollmentTokens(framework.internalUser, 1); const token = tokensDB[0]; @@ -81,7 +78,7 @@ describe('Token Domain Lib', () => { const almostTenMinutesFromNow = moment(tenMinutesFromNow) .subtract('2', 'seconds') .valueOf(); - expect(tokenExpiresOn).to.be.lessThan(tenMinutesFromNow); - expect(tokenExpiresOn).to.be.greaterThan(almostTenMinutesFromNow); + expect(tokenExpiresOn).toBeLessThan(tenMinutesFromNow); + expect(tokenExpiresOn).toBeGreaterThan(almostTenMinutesFromNow); }); }); diff --git a/x-pack/plugins/beats/server/lib/domains/beats.ts b/x-pack/plugins/beats/server/lib/domains/beats.ts index 0d5e068ff4ff7..9a580e2291ee7 100644 --- a/x-pack/plugins/beats/server/lib/domains/beats.ts +++ b/x-pack/plugins/beats/server/lib/domains/beats.ts @@ -4,24 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -/* - * 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 { uniq } from 'lodash'; import { findNonExistentItems } from '../../utils/find_non_existent_items'; +import { CMBeat } from '../../../common/domain_types'; import { - CMAssignmentReturn, - CMBeat, + BeatsTagAssignment, CMBeatsAdapter, - CMDomainLibs, - CMRemovalReturn, - CMTagAssignment, - FrameworkRequest, -} from '../lib'; +} from '../adapters/beats/adapter_types'; +import { FrameworkUser } from '../adapters/framework/adapter_types'; + +import { CMAssignmentReturn } from '../adapters/beats/adapter_types'; +import { CMDomainLibs } from '../lib'; +import { BeatsRemovalReturn } from './../adapters/beats/adapter_types'; export class CMBeatsDomain { private adapter: CMBeatsAdapter; @@ -85,9 +80,9 @@ export class CMBeatsDomain { } public async removeTagsFromBeats( - req: FrameworkRequest, - removals: CMTagAssignment[] - ): Promise { + user: FrameworkUser, + removals: BeatsTagAssignment[] + ): Promise { const beatIds = uniq(removals.map(removal => removal.beatId)); const tagIds = uniq(removals.map(removal => removal.tag)); @@ -95,8 +90,8 @@ export class CMBeatsDomain { removals: removals.map(() => ({ status: null })), }; - const beats = await this.adapter.getWithIds(req, beatIds); - const tags = await this.tags.getTagsWithIds(req, tagIds); + const beats = await this.adapter.getWithIds(user, beatIds); + const tags = await this.tags.getTagsWithIds(user, tagIds); // Handle assignments containing non-existing beat IDs or tags const nonExistentBeatIds = findNonExistentItems(beats, beatIds); @@ -121,7 +116,7 @@ export class CMBeatsDomain { if (validRemovals.length > 0) { const removalResults = await this.adapter.removeTagsFromBeats( - req, + user, validRemovals ); return addToResultsToResponse('removals', response, removalResults); @@ -129,13 +124,13 @@ export class CMBeatsDomain { return response; } - public async getAllBeats(req: FrameworkRequest) { - return await this.adapter.getAll(req); + public async getAllBeats(user: FrameworkUser) { + return await this.adapter.getAll(user); } // TODO cleanup return value, should return a status enum - public async verifyBeats(req: FrameworkRequest, beatIds: string[]) { - const beatsFromEs = await this.adapter.getWithIds(req, beatIds); + public async verifyBeats(user: FrameworkUser, beatIds: string[]) { + const beatsFromEs = await this.adapter.getWithIds(user, beatIds); const nonExistentBeatIds = findNonExistentItems(beatsFromEs, beatIds); @@ -148,7 +143,7 @@ export class CMBeatsDomain { .map((beat: any) => beat.id); const verifications = await this.adapter.verifyBeats( - req, + user, toBeVerifiedBeatIds ); @@ -161,8 +156,8 @@ export class CMBeatsDomain { } public async assignTagsToBeats( - req: FrameworkRequest, - assignments: CMTagAssignment[] + user: FrameworkUser, + assignments: BeatsTagAssignment[] ): Promise { const beatIds = uniq(assignments.map(assignment => assignment.beatId)); const tagIds = uniq(assignments.map(assignment => assignment.tag)); @@ -170,8 +165,8 @@ export class CMBeatsDomain { const response = { assignments: assignments.map(() => ({ status: null })), }; - const beats = await this.adapter.getWithIds(req, beatIds); - const tags = await this.tags.getTagsWithIds(req, tagIds); + const beats = await this.adapter.getWithIds(user, beatIds); + const tags = await this.tags.getTagsWithIds(user, tagIds); // Handle assignments containing non-existing beat IDs or tags const nonExistentBeatIds = findNonExistentItems(beats, beatIds); const nonExistentTags = findNonExistentItems(tags, tagIds); @@ -197,7 +192,7 @@ export class CMBeatsDomain { if (validAssignments.length > 0) { const assignmentResults = await this.adapter.assignTagsToBeats( - req, + user, validAssignments ); @@ -216,7 +211,7 @@ function addNonExistentItemToResponse( nonExistentTags: any, key: string ) { - assignments.forEach(({ beatId, tag }: CMTagAssignment, idx: any) => { + assignments.forEach(({ beatId, tag }: BeatsTagAssignment, idx: any) => { const isBeatNonExistent = nonExistentBeatIds.includes(beatId); const isTagNonExistent = nonExistentTags.includes(tag); diff --git a/x-pack/plugins/beats/server/lib/domains/tags.ts b/x-pack/plugins/beats/server/lib/domains/tags.ts index 43bb8dfed15a1..194551721ab33 100644 --- a/x-pack/plugins/beats/server/lib/domains/tags.ts +++ b/x-pack/plugins/beats/server/lib/domains/tags.ts @@ -6,7 +6,10 @@ import { intersection, uniq, values } from 'lodash'; import { UNIQUENESS_ENFORCING_TYPES } from '../../../common/constants'; -import { CMTagsAdapter, ConfigurationBlock, FrameworkRequest } from '../lib'; +import { ConfigurationBlock } from '../../../common/domain_types'; +import { FrameworkUser } from '../adapters/framework/adapter_types'; + +import { CMTagsAdapter } from '../adapters/tags/adapter_types'; import { entries } from './../../utils/polyfills'; export class CMTagsDomain { @@ -15,12 +18,12 @@ export class CMTagsDomain { this.adapter = adapter; } - public async getTagsWithIds(req: FrameworkRequest, tagIds: string[]) { - return await this.adapter.getTagsWithIds(req, tagIds); + public async getTagsWithIds(user: FrameworkUser, tagIds: string[]) { + return await this.adapter.getTagsWithIds(user, tagIds); } public async saveTag( - req: FrameworkRequest, + user: FrameworkUser, tagId: string, configs: ConfigurationBlock[] ) { @@ -37,7 +40,7 @@ export class CMTagsDomain { }; return { isValid: true, - result: await this.adapter.upsertTag(req, tag), + result: await this.adapter.upsertTag(user, tag), }; } diff --git a/x-pack/plugins/beats/server/lib/domains/tokens.ts b/x-pack/plugins/beats/server/lib/domains/tokens.ts index b2a9d283e484a..6cfb922c34d57 100644 --- a/x-pack/plugins/beats/server/lib/domains/tokens.ts +++ b/x-pack/plugins/beats/server/lib/domains/tokens.ts @@ -3,13 +3,13 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - import { timingSafeEqual } from 'crypto'; import { sign as signToken, verify as verifyToken } from 'jsonwebtoken'; import moment from 'moment'; import uuid from 'uuid'; -import { CMTokensAdapter, FrameworkRequest } from '../lib'; -import { BackendFrameworkAdapter } from '../lib'; +import { BackendFrameworkAdapter } from '../adapters/framework/adapter_types'; +import { CMTokensAdapter } from '../adapters/tokens/adapter_types'; +import { FrameworkUser } from './../adapters/framework/adapter_types'; const RANDOM_TOKEN_1 = 'b48c4bda384a40cb91c6eb9b8849e77f'; const RANDOM_TOKEN_2 = '80a3819e3cd64f4399f1d4886be7a08b'; @@ -120,7 +120,7 @@ export class CMTokensDomain { } public async createEnrollmentTokens( - req: FrameworkRequest, + user: FrameworkUser, numTokens: number = 1 ): Promise { const tokens = []; @@ -139,7 +139,7 @@ export class CMTokensDomain { }); } - await this.adapter.upsertTokens(req, tokens); + await this.adapter.upsertTokens(user, tokens); return tokens.map(token => token.token); } diff --git a/x-pack/plugins/beats/server/lib/lib.ts b/x-pack/plugins/beats/server/lib/lib.ts index 6aab0acd733d8..d916c18aa4e4a 100644 --- a/x-pack/plugins/beats/server/lib/lib.ts +++ b/x-pack/plugins/beats/server/lib/lib.ts @@ -4,14 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IRouteAdditionalConfigurationOptions, IStrictReply } from 'hapi'; -import { internalFrameworkRequest } from '../utils/wrap_request'; +import { DatabaseAdapter } from './adapters/database/adapter_types'; +import { BackendFrameworkAdapter } from './adapters/framework/adapter_types'; import { CMBeatsDomain } from './domains/beats'; import { CMTagsDomain } from './domains/tags'; import { CMTokensDomain } from './domains/tokens'; -import { ConfigurationBlockTypes } from '../../common/constants'; - export interface CMDomainLibs { beats: CMBeatsDomain; tags: CMTagsDomain; @@ -20,195 +18,5 @@ export interface CMDomainLibs { export interface CMServerLibs extends CMDomainLibs { framework: BackendFrameworkAdapter; -} - -interface CMReturnedTagAssignment { - status: number | null; - result?: string; -} - -export interface CMAssignmentReturn { - assignments: CMReturnedTagAssignment[]; -} - -export interface CMRemovalReturn { - removals: CMReturnedTagAssignment[]; -} - -export interface ConfigurationBlock { - type: ConfigurationBlockTypes; - block_yml: string; -} - -export interface CMBeat { - id: string; - access_token: string; - verified_on?: string; - type: string; - version?: string; - host_ip: string; - host_name: string; - ephemeral_id?: string; - local_configuration_yml?: string; - tags?: string[]; - central_configuration_yml?: string; - metadata?: {}; -} - -export interface BeatTag { - id: string; - configuration_blocks: ConfigurationBlock[]; -} - -export interface EnrollmentToken { - token: string | null; - expires_on: string; -} - -export interface CMTokensAdapter { - deleteEnrollmentToken(enrollmentToken: string): Promise; - getEnrollmentToken(enrollmentToken: string): Promise; - upsertTokens( - req: FrameworkRequest, - tokens: EnrollmentToken[] - ): Promise; -} - -// FIXME: fix getTagsWithIds return type -export interface CMTagsAdapter { - getTagsWithIds(req: FrameworkRequest, tagIds: string[]): any; - upsertTag(req: FrameworkRequest, tag: BeatTag): Promise<{}>; -} - -// FIXME: fix getBeatsWithIds return type -export interface CMBeatsAdapter { - insert(beat: CMBeat): Promise; - update(beat: CMBeat): Promise; - get(id: string): any; - getAll(req: FrameworkRequest): any; - getWithIds(req: FrameworkRequest, beatIds: string[]): any; - verifyBeats(req: FrameworkRequest, beatIds: string[]): any; - removeTagsFromBeats( - req: FrameworkRequest, - removals: CMTagAssignment[] - ): Promise; - assignTagsToBeats( - req: FrameworkRequest, - assignments: CMTagAssignment[] - ): Promise; -} - -export interface CMTagAssignment { - beatId: string; - tag: string; - idxInRequest?: number; -} - -/** - * The following are generic types, sharable between projects - */ - -export interface BackendFrameworkAdapter { - version: string; - getSetting(settingPath: string): any; - exposeStaticDir(urlPath: string, dir: string): void; - installIndexTemplate(name: string, template: {}): void; - registerRoute( - route: FrameworkRouteOptions - ): void; - callWithInternalUser(esMethod: string, options: {}): Promise; - callWithRequest( - req: FrameworkRequest, - method: 'search', - options?: object - ): Promise>; - callWithRequest( - req: FrameworkRequest, - method: 'fieldCaps', - options?: object - ): Promise; - callWithRequest( - req: FrameworkRequest, - method: string, - options?: object - ): Promise; -} - -interface DatabaseFieldCapsResponse extends DatabaseResponse { - fields: FieldsResponse; -} - -export interface FieldsResponse { - [name: string]: FieldDef; -} - -export interface FieldDetails { - searchable: boolean; - aggregatable: boolean; - type: string; -} - -export interface FieldDef { - [type: string]: FieldDetails; -} - -export interface FrameworkRequest< - InternalRequest extends WrappableRequest = WrappableRequest -> { - [internalFrameworkRequest]: InternalRequest; - headers: InternalRequest['headers']; - info: InternalRequest['info']; - payload: InternalRequest['payload']; - params: InternalRequest['params']; - query: InternalRequest['query']; -} - -export interface FrameworkRouteOptions< - RouteRequest extends WrappableRequest, - RouteResponse -> { - path: string; - method: string | string[]; - vhost?: string; - handler: FrameworkRouteHandler; - config?: Pick< - IRouteAdditionalConfigurationOptions, - Exclude - >; -} - -export type FrameworkRouteHandler< - RouteRequest extends WrappableRequest, - RouteResponse -> = ( - request: FrameworkRequest, - reply: IStrictReply -) => void; - -export interface WrappableRequest< - Payload = any, - Params = any, - Query = any, - Headers = any, - Info = any -> { - headers: Headers; - info: Info; - payload: Payload; - params: Params; - query: Query; -} - -interface DatabaseResponse { - took: number; - timeout: boolean; -} - -interface DatabaseSearchResponse - extends DatabaseResponse { - aggregations?: Aggregations; - hits: { - total: number; - hits: Hit[]; - }; + database: DatabaseAdapter; } diff --git a/x-pack/plugins/beats/server/management_server.ts b/x-pack/plugins/beats/server/management_server.ts index ed0917eda8ced..637da2e37bd07 100644 --- a/x-pack/plugins/beats/server/management_server.ts +++ b/x-pack/plugins/beats/server/management_server.ts @@ -17,7 +17,10 @@ import { createTokensRoute } from './rest_api/tokens/create'; import { beatsIndexTemplate } from './utils/index_templates'; export const initManagementServer = (libs: CMServerLibs) => { - libs.framework.installIndexTemplate('beats-template', beatsIndexTemplate); + libs.database.putTemplate(libs.framework.internalUser, { + name: 'beats-template', + body: beatsIndexTemplate, + }); libs.framework.registerRoute(createTagAssignmentsRoute(libs)); libs.framework.registerRoute(createListAgentsRoute(libs)); diff --git a/x-pack/plugins/beats/server/rest_api/beats/enroll.ts b/x-pack/plugins/beats/server/rest_api/beats/enroll.ts index c86e5272e1e23..d909b64810360 100644 --- a/x-pack/plugins/beats/server/rest_api/beats/enroll.ts +++ b/x-pack/plugins/beats/server/rest_api/beats/enroll.ts @@ -7,6 +7,7 @@ import Joi from 'joi'; import { omit } from 'lodash'; import moment from 'moment'; +import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; import { CMServerLibs } from '../../lib/lib'; import { wrapEsError } from '../../utils/error_wrappers'; @@ -28,7 +29,7 @@ export const createBeatEnrollmentRoute = (libs: CMServerLibs) => ({ }).required(), }, }, - handler: async (request: any, reply: any) => { + handler: async (request: FrameworkRequest, reply: any) => { const { beatId } = request.params; const enrollmentToken = request.headers['kbn-beats-enrollment-token']; diff --git a/x-pack/plugins/beats/server/rest_api/beats/list.ts b/x-pack/plugins/beats/server/rest_api/beats/list.ts index 8263d1c0ff63f..a47bfcfbf3853 100644 --- a/x-pack/plugins/beats/server/rest_api/beats/list.ts +++ b/x-pack/plugins/beats/server/rest_api/beats/list.ts @@ -4,14 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ +import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; import { CMServerLibs } from '../../lib/lib'; import { wrapEsError } from '../../utils/error_wrappers'; // TODO: add license check pre-hook export const createListAgentsRoute = (libs: CMServerLibs) => ({ - handler: async (request: any, reply: any) => { + handler: async (request: FrameworkRequest, reply: any) => { try { - const beats = await libs.beats.getAllBeats(request); + const beats = await libs.beats.getAllBeats(request.user); reply({ beats }); } catch (err) { // TODO move this to kibana route thing in adapter diff --git a/x-pack/plugins/beats/server/rest_api/beats/tag_assignment.ts b/x-pack/plugins/beats/server/rest_api/beats/tag_assignment.ts index d06c016ce6d12..a6654250d196f 100644 --- a/x-pack/plugins/beats/server/rest_api/beats/tag_assignment.ts +++ b/x-pack/plugins/beats/server/rest_api/beats/tag_assignment.ts @@ -5,6 +5,7 @@ */ import Joi from 'joi'; +import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; import { CMServerLibs } from '../../lib/lib'; import { wrapEsError } from '../../utils/error_wrappers'; @@ -23,7 +24,7 @@ export const createTagAssignmentsRoute = (libs: CMServerLibs) => ({ }).required(), }, }, - handler: async (request: any, reply: any) => { + handler: async (request: FrameworkRequest, reply: any) => { const { assignments } = request.payload; // TODO abstract or change API to keep beatId consistent @@ -34,7 +35,7 @@ export const createTagAssignmentsRoute = (libs: CMServerLibs) => ({ try { const response = await libs.beats.assignTagsToBeats( - request, + request.user, tweakedAssignments ); reply(response); diff --git a/x-pack/plugins/beats/server/rest_api/beats/tag_removal.ts b/x-pack/plugins/beats/server/rest_api/beats/tag_removal.ts index 4da33dbd50cfc..1b495b906a364 100644 --- a/x-pack/plugins/beats/server/rest_api/beats/tag_removal.ts +++ b/x-pack/plugins/beats/server/rest_api/beats/tag_removal.ts @@ -5,6 +5,7 @@ */ import Joi from 'joi'; +import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; import { CMServerLibs } from '../../lib/lib'; import { wrapEsError } from '../../utils/error_wrappers'; @@ -23,7 +24,7 @@ export const createTagRemovalsRoute = (libs: CMServerLibs) => ({ }).required(), }, }, - handler: async (request: any, reply: any) => { + handler: async (request: FrameworkRequest, reply: any) => { const { removals } = request.payload; // TODO abstract or change API to keep beatId consistent @@ -34,7 +35,7 @@ export const createTagRemovalsRoute = (libs: CMServerLibs) => ({ try { const response = await libs.beats.removeTagsFromBeats( - request, + request.user, tweakedRemovals ); reply(response); diff --git a/x-pack/plugins/beats/server/rest_api/beats/update.ts b/x-pack/plugins/beats/server/rest_api/beats/update.ts index 3683c02ca2ccb..c86cf74b0c744 100644 --- a/x-pack/plugins/beats/server/rest_api/beats/update.ts +++ b/x-pack/plugins/beats/server/rest_api/beats/update.ts @@ -5,6 +5,7 @@ */ import Joi from 'joi'; +import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; import { CMServerLibs } from '../../lib/lib'; import { wrapEsError } from '../../utils/error_wrappers'; @@ -32,7 +33,7 @@ export const createBeatUpdateRoute = (libs: CMServerLibs) => ({ }).required(), }, }, - handler: async (request: any, reply: any) => { + handler: async (request: FrameworkRequest, reply: any) => { const { beatId } = request.params; const accessToken = request.headers['kbn-beats-access-token']; const remoteAddress = request.info.remoteAddress; diff --git a/x-pack/plugins/beats/server/rest_api/beats/verify.ts b/x-pack/plugins/beats/server/rest_api/beats/verify.ts index 7dba7f4e20692..d15b0374f445f 100644 --- a/x-pack/plugins/beats/server/rest_api/beats/verify.ts +++ b/x-pack/plugins/beats/server/rest_api/beats/verify.ts @@ -5,6 +5,7 @@ */ import Joi from 'joi'; +import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; import { CMServerLibs } from '../../lib/lib'; import { wrapEsError } from '../../utils/error_wrappers'; @@ -23,7 +24,7 @@ export const createBeatVerificationRoute = (libs: CMServerLibs) => ({ }).required(), }, }, - handler: async (request: any, reply: any) => { + handler: async (request: FrameworkRequest, reply: any) => { const beats = [...request.payload.beats]; const beatIds = beats.map(beat => beat.id); @@ -32,7 +33,7 @@ export const createBeatVerificationRoute = (libs: CMServerLibs) => ({ verifiedBeatIds, alreadyVerifiedBeatIds, nonExistentBeatIds, - } = await libs.beats.verifyBeats(request, beatIds); + } = await libs.beats.verifyBeats(request.user, beatIds); // TODO calculation of status should be done in-lib, w/switch statement here beats.forEach(beat => { diff --git a/x-pack/plugins/beats/server/rest_api/tags/set.ts b/x-pack/plugins/beats/server/rest_api/tags/set.ts index 3f7e579bd91ae..bdc49ce62a9a9 100644 --- a/x-pack/plugins/beats/server/rest_api/tags/set.ts +++ b/x-pack/plugins/beats/server/rest_api/tags/set.ts @@ -7,6 +7,7 @@ import Joi from 'joi'; import { get, values } from 'lodash'; import { ConfigurationBlockTypes } from '../../../common/constants'; +import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; import { CMServerLibs } from '../../lib/lib'; import { wrapEsError } from '../../utils/error_wrappers'; @@ -30,7 +31,7 @@ export const createSetTagRoute = (libs: CMServerLibs) => ({ }).allow(null), }, }, - handler: async (request: any, reply: any) => { + handler: async (request: FrameworkRequest, reply: any) => { const configurationBlocks = get( request, 'payload.configuration_blocks', @@ -38,7 +39,7 @@ export const createSetTagRoute = (libs: CMServerLibs) => ({ ); try { const { isValid, result } = await libs.tags.saveTag( - request, + request.user, request.params.tag, configurationBlocks ); diff --git a/x-pack/plugins/beats/server/rest_api/tokens/create.ts b/x-pack/plugins/beats/server/rest_api/tokens/create.ts index b4f3e2c1a6246..9e20735d640e5 100644 --- a/x-pack/plugins/beats/server/rest_api/tokens/create.ts +++ b/x-pack/plugins/beats/server/rest_api/tokens/create.ts @@ -6,9 +6,9 @@ import Joi from 'joi'; import { get } from 'lodash'; +import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; import { CMServerLibs } from '../../lib/lib'; import { wrapEsError } from '../../utils/error_wrappers'; - // TODO: add license check pre-hook // TODO: write to Kibana audit log file const DEFAULT_NUM_TOKENS = 1; @@ -23,12 +23,12 @@ export const createTokensRoute = (libs: CMServerLibs) => ({ }).allow(null), }, }, - handler: async (request: any, reply: any) => { + handler: async (request: FrameworkRequest, reply: any) => { const numTokens = get(request, 'payload.num_tokens', DEFAULT_NUM_TOKENS); try { const tokens = await libs.tokens.createEnrollmentTokens( - request, + request.user, numTokens ); reply({ tokens }); diff --git a/x-pack/plugins/beats/server/utils/error_wrappers/wrap_es_error.test.js b/x-pack/plugins/beats/server/utils/error_wrappers/wrap_es_error.test.ts similarity index 73% rename from x-pack/plugins/beats/server/utils/error_wrappers/wrap_es_error.test.js rename to x-pack/plugins/beats/server/utils/error_wrappers/wrap_es_error.test.ts index de79815258f7a..5087bf3224c42 100644 --- a/x-pack/plugins/beats/server/utils/error_wrappers/wrap_es_error.test.js +++ b/x-pack/plugins/beats/server/utils/error_wrappers/wrap_es_error.test.ts @@ -4,12 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from 'expect.js'; import { wrapEsError } from './wrap_es_error'; describe('wrap_es_error', () => { describe('#wrapEsError', () => { - let originalError; + let originalError: any; beforeEach(() => { originalError = new Error('I am an error'); originalError.statusCode = 404; @@ -18,23 +17,26 @@ describe('wrap_es_error', () => { it('should return a Boom object', () => { const wrappedError = wrapEsError(originalError); - expect(wrappedError.isBoom).to.be(true); + expect(wrappedError.isBoom).toEqual(true); }); it('should return the correct Boom object', () => { const wrappedError = wrapEsError(originalError); - expect(wrappedError.output.statusCode).to.be(originalError.statusCode); - expect(wrappedError.output.payload.message).to.be(originalError.message); + expect(wrappedError.output.statusCode).toEqual(originalError.statusCode); + expect(wrappedError.output.payload.message).toEqual( + originalError.message + ); }); it('should return invalid permissions message for 403 errors', () => { const securityError = new Error('I am an error'); + // @ts-ignore securityError.statusCode = 403; const wrappedError = wrapEsError(securityError); - expect(wrappedError.isBoom).to.be(true); - expect(wrappedError.message).to.be( + expect(wrappedError.isBoom).toEqual(true); + expect(wrappedError.message).toEqual( 'Insufficient user permissions for managing Beats configuration' ); }); diff --git a/x-pack/plugins/beats/server/utils/wrap_request.ts b/x-pack/plugins/beats/server/utils/wrap_request.ts index a29f9055f3688..66bdbf578e141 100644 --- a/x-pack/plugins/beats/server/utils/wrap_request.ts +++ b/x-pack/plugins/beats/server/utils/wrap_request.ts @@ -4,17 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FrameworkRequest, WrappableRequest } from '../lib/lib'; +import { + FrameworkRequest, + FrameworkWrappableRequest, +} from '../lib/adapters/framework/adapter_types'; -export const internalFrameworkRequest = Symbol('internalFrameworkRequest'); +export const internalAuthData = Symbol('internalAuthData'); -export function wrapRequest( +export function wrapRequest( req: InternalRequest ): FrameworkRequest { const { params, payload, query, headers, info } = req; + const isAuthenticated = headers.authorization != null; + return { - [internalFrameworkRequest]: req, + user: isAuthenticated + ? { + kind: 'authenticated', + [internalAuthData]: headers, + } + : { + kind: 'unauthenticated', + }, headers, info, params, diff --git a/x-pack/plugins/beats/wallaby.js b/x-pack/plugins/beats/wallaby.js index 8c0c4aa355925..79b1438a5374d 100644 --- a/x-pack/plugins/beats/wallaby.js +++ b/x-pack/plugins/beats/wallaby.js @@ -4,10 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ const path = require('path'); -process.env.NODE_PATH = path.join(__dirname, '..', '..', 'node_modules'); +process.env.NODE_PATH += + path.delimiter + path.join(__dirname, '..', '..', '..', 'node_modules'); module.exports = function (wallaby) { return { + hints: { + commentAutoLog: 'testOutputWith:', + }, debug: true, files: [ './tsconfig.json', @@ -39,6 +43,7 @@ module.exports = function (wallaby) { '..', '..' ); + wallaby.testFramework.configure({ rootDir: wallaby.localProjectDir, moduleNameMapper: { @@ -56,7 +61,6 @@ module.exports = function (wallaby) { ], transform: { '^.+\\.js$': `${kibanaDirectory}/src/dev/jest/babel_transform.js`, - //"^.+\\.tsx?$": `${kibanaDirectory}/src/dev/jest/ts_transform.js`, }, }); }, diff --git a/x-pack/yarn.lock b/x-pack/yarn.lock index c7a1d0ec50830..3fc10b7dd6909 100644 --- a/x-pack/yarn.lock +++ b/x-pack/yarn.lock @@ -143,12 +143,6 @@ dependencies: "@types/node" "*" -"@types/hapi@15.0.1": - version "15.0.1" - resolved "https://registry.yarnpkg.com/@types/hapi/-/hapi-15.0.1.tgz#919e1d3a9160a080c9fdefaccc892239772e1258" - dependencies: - "@types/node" "*" - "@types/is-stream@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@types/is-stream/-/is-stream-1.1.0.tgz#b84d7bb207a210f2af9bed431dc0fbe9c4143be1" From d2aa659860f4d868753517b8baa5da7da680e99f Mon Sep 17 00:00:00 2001 From: Matt Apperson Date: Fri, 13 Jul 2018 11:32:10 -0400 Subject: [PATCH 22/43] [Beats Management] add get beat endpoint (#20603) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Beats Management] Move tokens to use JWT, add more complete test suite (#20317) * inital effort to move to JWT and added jest based tests on libs * assign beats tests all passing * token tests now pass * add more tests * all tests now green * fix broken test, this is beats CM not logstash 😊 * added readme * move enrollment token back to a hash * remove un-needed comment * alias lodash get to avoid confusion * isolated hash creation * inital effort to move to JWT and added jest based tests on libs * assign beats tests all passing * token tests now pass * add more tests * all tests now green * move enrollment token back to a hash * remove un-needed comment * alias lodash get to avoid confusion * isolated hash creation * Add initial efforts for backend framework adapter testing * move ES code to a DatabaseAdapter from BackendAdapter and add a TON of types for ES * re-typed * renamed types to match pattern * aditional renames * adapter tests should always just use adapterSetup(); * database now uses InternalRequest * corrected spelling of framework * fix typings * remove CRUFT * RequestOrInternal * Dont pass around request objects everywhere, just pass the user. Also, removed hapi types as they were not compatible * fix tests, add test, removed extra comment * Moved critical path code from route, to more easeley tested domain * fix auth * remove beat verification, added get beat endpoint to return configs * fix type * update createGetBeatConfigurationRoute URL * rename method * update to match PR #20566 * updated lock file * fix bad merge * update TSLinting --- .../plugins/beats/common/constants/index.ts | 5 +- x-pack/plugins/beats/index.ts | 22 +-- x-pack/plugins/beats/readme.md | 10 +- .../beats/elasticsearch_beats_adapter.ts | 29 ++- .../adapters/beats/memory_beats_adapter.ts | 36 ++-- .../database/__tests__/kibana.test.ts | 3 +- .../lib/adapters/database/adapter_types.ts | 34 +--- .../database/kibana_database_adapter.ts | 15 +- .../kibana/kibana_framework_adapter.ts | 99 ++++++++++ .../kibana/testing_framework_adapter.ts | 79 ++++++++ .../framework/__tests__/kibana.test.ts | 5 +- .../lib/adapters/framework/adapter_types.ts | 8 +- .../framework/kibana_framework_adapter.ts | 10 +- .../framework/testing_framework_adapter.ts | 13 +- .../lib/adapters/tokens/adapter_types.ts | 5 +- .../tokens/elasticsearch_tokens_adapter.ts | 31 +-- .../adapters/tokens/memory_tokens_adapter.ts | 17 +- .../beats/server/lib/compose/kibana.ts | 22 +-- .../__tests__/beats/assign_tags.test.ts | 8 +- .../domains/__tests__/beats/enroll.test.ts | 13 +- .../domains/__tests__/beats/verify.test.ts | 183 ------------------ .../lib/domains/__tests__/tokens.test.ts | 14 +- .../plugins/beats/server/lib/domains/beats.ts | 85 +++----- .../plugins/beats/server/lib/domains/tags.ts | 15 +- .../beats/server/lib/domains/tokens.ts | 25 +-- x-pack/plugins/beats/server/lib/lib.ts | 6 + .../plugins/beats/server/management_server.ts | 5 +- .../server/rest_api/beats/configuration.ts | 54 ++++++ .../beats/server/rest_api/beats/enroll.ts | 32 +-- .../server/rest_api/beats/tag_assignment.ts | 5 +- .../server/rest_api/beats/tag_removal.ts | 5 +- .../beats/server/rest_api/beats/update.ts | 2 - .../beats/server/rest_api/beats/verify.ts | 63 ------ .../plugins/beats/server/rest_api/tags/set.ts | 6 +- .../beats/server/rest_api/tokens/create.ts | 5 +- .../error_wrappers/wrap_es_error.test.ts | 4 +- .../utils/error_wrappers/wrap_es_error.ts | 4 +- .../server/utils/find_non_existent_items.ts | 19 +- x-pack/plugins/beats/wallaby.js | 8 +- .../error_wrappers/__tests__/wrap_es_error.js | 41 ---- .../api_integration/apis/beats/enroll_beat.js | 2 +- .../api_integration/apis/beats/get_beat.js | 51 +++++ .../test/api_integration/apis/beats/index.js | 4 +- .../api_integration/apis/beats/update_beat.js | 30 --- .../apis/beats/verify_beats.js | 66 ------- .../es_archives/beats/list/data.json | 139 +++++++++++++ .../es_archives/beats/list/data.json.gz | Bin 521 -> 0 bytes .../es_archives/beats/list/mappings.json | 5 +- x-pack/yarn.lock | 4 + 49 files changed, 600 insertions(+), 746 deletions(-) create mode 100644 x-pack/plugins/beats/server/lib/adapters/famework/kibana/kibana_framework_adapter.ts create mode 100644 x-pack/plugins/beats/server/lib/adapters/famework/kibana/testing_framework_adapter.ts delete mode 100644 x-pack/plugins/beats/server/lib/domains/__tests__/beats/verify.test.ts create mode 100644 x-pack/plugins/beats/server/rest_api/beats/configuration.ts delete mode 100644 x-pack/plugins/beats/server/rest_api/beats/verify.ts delete mode 100644 x-pack/plugins/logstash/server/lib/error_wrappers/__tests__/wrap_es_error.js create mode 100644 x-pack/test/api_integration/apis/beats/get_beat.js delete mode 100644 x-pack/test/api_integration/apis/beats/verify_beats.js create mode 100644 x-pack/test/functional/es_archives/beats/list/data.json delete mode 100644 x-pack/test/functional/es_archives/beats/list/data.json.gz diff --git a/x-pack/plugins/beats/common/constants/index.ts b/x-pack/plugins/beats/common/constants/index.ts index 4662865e208a7..756ffcf07e3ea 100644 --- a/x-pack/plugins/beats/common/constants/index.ts +++ b/x-pack/plugins/beats/common/constants/index.ts @@ -6,7 +6,4 @@ export { PLUGIN } from './plugin'; export { INDEX_NAMES } from './index_names'; -export { - UNIQUENESS_ENFORCING_TYPES, - ConfigurationBlockTypes, -} from './configuration_blocks'; +export { UNIQUENESS_ENFORCING_TYPES, ConfigurationBlockTypes } from './configuration_blocks'; diff --git a/x-pack/plugins/beats/index.ts b/x-pack/plugins/beats/index.ts index 25be5728c93bb..ced89c186f73e 100644 --- a/x-pack/plugins/beats/index.ts +++ b/x-pack/plugins/beats/index.ts @@ -10,20 +10,18 @@ import { initServerWithKibana } from './server/kibana.index'; const DEFAULT_ENROLLMENT_TOKENS_TTL_S = 10 * 60; // 10 minutes -export const config = Joi.object({ - enabled: Joi.boolean().default(true), - encryptionKey: Joi.string(), - enrollmentTokensTtlInSeconds: Joi.number() - .integer() - .min(1) - .default(DEFAULT_ENROLLMENT_TOKENS_TTL_S), -}).default(); -export const configPrefix = 'xpack.beats'; - export function beats(kibana: any) { return new kibana.Plugin({ - config: () => config, - configPrefix, + config: () => + Joi.object({ + enabled: Joi.boolean().default(true), + encryptionKey: Joi.string(), + enrollmentTokensTtlInSeconds: Joi.number() + .integer() + .min(1) + .default(DEFAULT_ENROLLMENT_TOKENS_TTL_S), + }).default(), + configPrefix: 'xpack.beats', id: PLUGIN.ID, require: ['kibana', 'elasticsearch', 'xpack_main'], init(server: any) { diff --git a/x-pack/plugins/beats/readme.md b/x-pack/plugins/beats/readme.md index 725fe587c5aa8..fdd56a393e573 100644 --- a/x-pack/plugins/beats/readme.md +++ b/x-pack/plugins/beats/readme.md @@ -1,15 +1,7 @@ # Documentation for Beats CM in x-pack kibana -### Run tests (from x-pack dir) - -Functional tests +### Run tests ``` node scripts/jest.js plugins/beats --watch ``` - -Functional API tests - -``` -node scripts/functional_tests --config test/api_integration/config -``` diff --git a/x-pack/plugins/beats/server/lib/adapters/beats/elasticsearch_beats_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/beats/elasticsearch_beats_adapter.ts index e2321ac6739a8..c0be74e1629f6 100644 --- a/x-pack/plugins/beats/server/lib/adapters/beats/elasticsearch_beats_adapter.ts +++ b/x-pack/plugins/beats/server/lib/adapters/beats/elasticsearch_beats_adapter.ts @@ -30,10 +30,7 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { type: '_doc', }; - const response = await this.database.get( - this.framework.internalUser, - params - ); + const response = await this.database.get(this.framework.internalUser, params); if (!response.found) { return null; } @@ -155,13 +152,11 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { refresh: 'wait_for', type: '_doc', }); - return _get(response, 'items', []).map( - (item: any, resultIdx: number) => ({ - idxInRequest: removals[resultIdx].idxInRequest, - result: item.update.result, - status: item.update.status, - }) - ); + return _get(response, 'items', []).map((item: any, resultIdx: number) => ({ + idxInRequest: removals[resultIdx].idxInRequest, + result: item.update.result, + status: item.update.status, + })); } public async assignTagsToBeats( @@ -193,12 +188,10 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { refresh: 'wait_for', type: '_doc', }); - return _get(response, 'items', []).map( - (item: any, resultIdx: any) => ({ - idxInRequest: assignments[resultIdx].idxInRequest, - result: item.update.result, - status: item.update.status, - }) - ); + return _get(response, 'items', []).map((item: any, resultIdx: any) => ({ + idxInRequest: assignments[resultIdx].idxInRequest, + result: item.update.result, + status: item.update.status, + })); } } diff --git a/x-pack/plugins/beats/server/lib/adapters/beats/memory_beats_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/beats/memory_beats_adapter.ts index a904b1d6831cd..041b34d29b49e 100644 --- a/x-pack/plugins/beats/server/lib/adapters/beats/memory_beats_adapter.ts +++ b/x-pack/plugins/beats/server/lib/adapters/beats/memory_beats_adapter.ts @@ -19,7 +19,7 @@ export class MemoryBeatsAdapter implements CMBeatsAdapter { } public async get(id: string) { - return this.beatsDB.find(beat => beat.id === id); + return this.beatsDB.find(beat => beat.id === id) || null; } public async insert(beat: CMBeat) { @@ -65,17 +65,15 @@ export class MemoryBeatsAdapter implements CMBeatsAdapter { ): Promise { const beatIds = removals.map(r => r.beatId); - const response = this.beatsDB - .filter(beat => beatIds.includes(beat.id)) - .map(beat => { - const tagData = removals.find(r => r.beatId === beat.id); - if (tagData) { - if (beat.tags) { - beat.tags = beat.tags.filter(tag => tag !== tagData.tag); - } + const response = this.beatsDB.filter(beat => beatIds.includes(beat.id)).map(beat => { + const tagData = removals.find(r => r.beatId === beat.id); + if (tagData) { + if (beat.tags) { + beat.tags = beat.tags.filter(tag => tag !== tagData.tag); } - return beat; - }); + } + return beat; + }); return response.map((item: CMBeat, resultIdx: number) => ({ idxInRequest: removals[resultIdx].idxInRequest, @@ -100,9 +98,7 @@ export class MemoryBeatsAdapter implements CMBeatsAdapter { if (!beat.tags) { beat.tags = []; } - const nonExistingTags = tags.filter( - (t: string) => beat.tags && !beat.tags.includes(t) - ); + const nonExistingTags = tags.filter((t: string) => beat.tags && !beat.tags.includes(t)); if (nonExistingTags.length > 0) { beat.tags = beat.tags.concat(nonExistingTags); @@ -111,12 +107,10 @@ export class MemoryBeatsAdapter implements CMBeatsAdapter { return beat; }); - return assignments.map( - (item: BeatsTagAssignment, resultIdx: number) => ({ - idxInRequest: assignments[resultIdx].idxInRequest, - result: 'updated', - status: 200, - }) - ); + return assignments.map((item: BeatsTagAssignment, resultIdx: number) => ({ + idxInRequest: assignments[resultIdx].idxInRequest, + result: 'updated', + status: 200, + })); } } diff --git a/x-pack/plugins/beats/server/lib/adapters/database/__tests__/kibana.test.ts b/x-pack/plugins/beats/server/lib/adapters/database/__tests__/kibana.test.ts index 19bf05c3c777e..c18c5b5bc4c76 100644 --- a/x-pack/plugins/beats/server/lib/adapters/database/__tests__/kibana.test.ts +++ b/x-pack/plugins/beats/server/lib/adapters/database/__tests__/kibana.test.ts @@ -28,7 +28,6 @@ contractTests('Kibana Database Adapter', { return await es.cleanup(); }, adapterSetup: () => { - return new KibanaDatabaseAdapter(kbnServer.server.plugins - .elasticsearch as DatabaseKbnESPlugin); + return new KibanaDatabaseAdapter(kbnServer.server.plugins.elasticsearch as DatabaseKbnESPlugin); }, }); diff --git a/x-pack/plugins/beats/server/lib/adapters/database/adapter_types.ts b/x-pack/plugins/beats/server/lib/adapters/database/adapter_types.ts index 36b5a35742bc9..22ee7a34066b3 100644 --- a/x-pack/plugins/beats/server/lib/adapters/database/adapter_types.ts +++ b/x-pack/plugins/beats/server/lib/adapters/database/adapter_types.ts @@ -5,10 +5,7 @@ */ import { FrameworkRequest, FrameworkUser } from '../framework/adapter_types'; export interface DatabaseAdapter { - putTemplate( - user: FrameworkUser, - params: DatabasePutTemplateParams - ): Promise; + putTemplate(user: FrameworkUser, params: DatabasePutTemplateParams): Promise; get( user: FrameworkUser, params: DatabaseGetParams @@ -25,27 +22,17 @@ export interface DatabaseAdapter { user: FrameworkUser, params: DatabaseDeleteDocumentParams ): Promise; - mget( - user: FrameworkUser, - params: DatabaseMGetParams - ): Promise>; + mget(user: FrameworkUser, params: DatabaseMGetParams): Promise>; bulk( user: FrameworkUser, params: DatabaseBulkIndexDocumentsParams ): Promise; - search( - user: FrameworkUser, - params: DatabaseSearchParams - ): Promise>; + search(user: FrameworkUser, params: DatabaseSearchParams): Promise>; } export interface DatabaseKbnESCluster { callWithInternalUser(esMethod: string, options: {}): Promise; - callWithRequest( - req: FrameworkRequest, - esMethod: string, - options: {} - ): Promise; + callWithRequest(req: FrameworkRequest, esMethod: string, options: {}): Promise; } export interface DatabaseKbnESPlugin { @@ -142,14 +129,11 @@ export interface DatabaseBulkResponse { took: number; errors: boolean; items: Array< - | DatabaseDeleteDocumentResponse - | DatabaseIndexDocumentResponse - | DatabaseUpdateDocumentResponse + DatabaseDeleteDocumentResponse | DatabaseIndexDocumentResponse | DatabaseUpdateDocumentResponse >; } -export interface DatabaseBulkIndexDocumentsParams - extends DatabaseGenericParams { +export interface DatabaseBulkIndexDocumentsParams extends DatabaseGenericParams { waitForActiveShards?: string; refresh?: DatabaseRefresh; routing?: string; @@ -299,11 +283,7 @@ export interface DatabaseGetParams extends DatabaseGenericParams { export type DatabaseNameList = string | string[] | boolean; export type DatabaseRefresh = boolean | 'true' | 'false' | 'wait_for' | ''; -export type DatabaseVersionType = - | 'internal' - | 'external' - | 'external_gte' - | 'force'; +export type DatabaseVersionType = 'internal' | 'external' | 'external_gte' | 'force'; export type ExpandWildcards = 'open' | 'closed' | 'none' | 'all'; export type DefaultOperator = 'AND' | 'OR'; export type DatabaseConflicts = 'abort' | 'proceed'; diff --git a/x-pack/plugins/beats/server/lib/adapters/database/kibana_database_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/database/kibana_database_adapter.ts index 6c4d96446abd4..ba593a793e34b 100644 --- a/x-pack/plugins/beats/server/lib/adapters/database/kibana_database_adapter.ts +++ b/x-pack/plugins/beats/server/lib/adapters/database/kibana_database_adapter.ts @@ -30,10 +30,7 @@ export class KibanaDatabaseAdapter implements DatabaseAdapter { constructor(kbnElasticSearch: DatabaseKbnESPlugin) { this.es = kbnElasticSearch.getCluster('admin'); } - public async putTemplate( - user: FrameworkUser, - params: DatabasePutTemplateParams - ): Promise { + public async putTemplate(user: FrameworkUser, params: DatabasePutTemplateParams): Promise { const callES = this.getCallType(user); const result = await callES('indices.putTemplate', params); return result; @@ -59,10 +56,7 @@ export class KibanaDatabaseAdapter implements DatabaseAdapter { // todo } - public async bulk( - user: FrameworkUser, - params: DatabaseBulkIndexDocumentsParams - ): Promise { + public async bulk(user: FrameworkUser, params: DatabaseBulkIndexDocumentsParams): Promise { const callES = this.getCallType(user); const result = await callES('bulk', params); return result; @@ -76,10 +70,7 @@ export class KibanaDatabaseAdapter implements DatabaseAdapter { const result = await callES('create', params); return result; } - public async index( - user: FrameworkUser, - params: DatabaseIndexDocumentParams - ): Promise { + public async index(user: FrameworkUser, params: DatabaseIndexDocumentParams): Promise { const callES = this.getCallType(user); const result = await callES('index', params); return result; diff --git a/x-pack/plugins/beats/server/lib/adapters/famework/kibana/kibana_framework_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/famework/kibana/kibana_framework_adapter.ts new file mode 100644 index 0000000000000..71703da8632ec --- /dev/null +++ b/x-pack/plugins/beats/server/lib/adapters/famework/kibana/kibana_framework_adapter.ts @@ -0,0 +1,99 @@ +/* + * 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 { + BackendFrameworkAdapter, + FrameworkRequest, + FrameworkRouteOptions, + WrappableRequest, +} from '../../../lib'; + +import { IStrictReply, Request, Server } from 'hapi'; +import { internalFrameworkRequest, wrapRequest } from '../../../../utils/wrap_request'; + +export class KibanaBackendFrameworkAdapter implements BackendFrameworkAdapter { + public version: string; + private server: Server; + private cryptoHash: string | null; + + constructor(hapiServer: Server) { + this.server = hapiServer; + this.version = hapiServer.plugins.kibana.status.plugin.version; + this.cryptoHash = null; + + this.validateConfig(); + } + + public getSetting(settingPath: string) { + // TODO type check server properly + if (settingPath === 'xpack.beats.encryptionKey') { + // @ts-ignore + return this.server.config().get(settingPath) || this.cryptoHash; + } + // @ts-ignore + return this.server.config().get(settingPath) || this.cryptoHash; + } + + public exposeStaticDir(urlPath: string, dir: string): void { + this.server.route({ + handler: { + directory: { + path: dir, + }, + }, + method: 'GET', + path: urlPath, + }); + } + + public registerRoute( + route: FrameworkRouteOptions + ) { + const wrappedHandler = (request: any, reply: IStrictReply) => + route.handler(wrapRequest(request), reply); + + this.server.route({ + config: route.config, + handler: wrappedHandler, + method: route.method, + path: route.path, + }); + } + + public installIndexTemplate(name: string, template: {}) { + return this.callWithInternalUser('indices.putTemplate', { + body: template, + name, + }); + } + + public async callWithInternalUser(esMethod: string, options: {}) { + const { elasticsearch } = this.server.plugins; + const { callWithInternalUser } = elasticsearch.getCluster('admin'); + return await callWithInternalUser(esMethod, options); + } + + public async callWithRequest(req: FrameworkRequest, ...rest: any[]) { + const internalRequest = req[internalFrameworkRequest]; + const { elasticsearch } = internalRequest.server.plugins; + const { callWithRequest } = elasticsearch.getCluster('data'); + const fields = await callWithRequest(internalRequest, ...rest); + return fields; + } + + private validateConfig() { + // @ts-ignore + const config = this.server.config(); + const encryptionKey = config.get('xpack.beats.encryptionKey'); + + if (!encryptionKey) { + this.server.log( + 'Using a default encryption key for xpack.beats.encryptionKey. It is recommended that you set xpack.beats.encryptionKey in kibana.yml with a unique token' + ); + this.cryptoHash = 'xpack_beats_default_encryptionKey'; + } + } +} diff --git a/x-pack/plugins/beats/server/lib/adapters/famework/kibana/testing_framework_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/famework/kibana/testing_framework_adapter.ts new file mode 100644 index 0000000000000..757464fa6cdc7 --- /dev/null +++ b/x-pack/plugins/beats/server/lib/adapters/famework/kibana/testing_framework_adapter.ts @@ -0,0 +1,79 @@ +/* + * 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 { Client } from 'elasticsearch'; +import { Request } from 'hapi'; +import { get } from 'lodash'; +import { + BackendFrameworkAdapter, + FrameworkRequest, + FrameworkRouteOptions, + WrappableRequest, +} from '../../../lib'; + +interface TestSettings { + enrollmentTokensTtlInSeconds: number; + encryptionKey: string; +} + +export class TestingBackendFrameworkAdapter implements BackendFrameworkAdapter { + public version: string; + private client: Client | null; + private settings: TestSettings; + + constructor(client: Client | null, settings: TestSettings) { + this.client = client; + this.settings = settings || { + encryptionKey: 'something_who_cares', + enrollmentTokensTtlInSeconds: 10 * 60, // 10 minutes + }; + this.version = 'testing'; + } + + public getSetting(settingPath: string) { + switch (settingPath) { + case 'xpack.beats.enrollmentTokensTtlInSeconds': + return this.settings.enrollmentTokensTtlInSeconds; + case 'xpack.beats.encryptionKey': + return this.settings.encryptionKey; + } + } + + public exposeStaticDir(urlPath: string, dir: string): void { + // not yet testable + } + + public registerRoute( + route: FrameworkRouteOptions + ) { + // not yet testable + } + + public installIndexTemplate(name: string, template: {}) { + if (this.client) { + return this.client.indices.putTemplate({ + body: template, + name, + }); + } + } + + public async callWithInternalUser(esMethod: string, options: {}) { + const api = get(this.client, esMethod); + + api(options); + + return await api(options); + } + + public async callWithRequest(req: FrameworkRequest, esMethod: string, options: {}) { + const api = get(this.client, esMethod); + + api(options); + + return await api(options); + } +} diff --git a/x-pack/plugins/beats/server/lib/adapters/framework/__tests__/kibana.test.ts b/x-pack/plugins/beats/server/lib/adapters/framework/__tests__/kibana.test.ts index c87a7374837e3..5a539ebe6e5e7 100644 --- a/x-pack/plugins/beats/server/lib/adapters/framework/__tests__/kibana.test.ts +++ b/x-pack/plugins/beats/server/lib/adapters/framework/__tests__/kibana.test.ts @@ -7,12 +7,9 @@ // @ts-ignore import { createEsTestCluster } from '@kbn/test'; +import { config as beatsPluginConfig, configPrefix } from '../../../../..'; // @ts-ignore import * as kbnTestServer from '../../../../../../../../src/test_utils/kbn_server'; -import { - config as beatsPluginConfig, - configPrefix, -} from '../../../../../index'; import { KibanaBackendFrameworkAdapter } from '../kibana_framework_adapter'; import { contractTests } from './test_contract'; diff --git a/x-pack/plugins/beats/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/beats/server/lib/adapters/framework/adapter_types.ts index 4be3589a6f043..014c041b11f0a 100644 --- a/x-pack/plugins/beats/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/beats/server/lib/adapters/framework/adapter_types.ts @@ -54,10 +54,10 @@ export interface FrameworkRouteOptions< config?: {}; } -export type FrameworkRouteHandler< - RouteRequest extends FrameworkWrappableRequest, - RouteResponse -> = (request: FrameworkRequest, reply: any) => void; +export type FrameworkRouteHandler = ( + request: FrameworkRequest, + reply: any +) => void; export interface FrameworkWrappableRequest< Payload = any, diff --git a/x-pack/plugins/beats/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/framework/kibana_framework_adapter.ts index 7113baf5c26e6..d07170acb7050 100644 --- a/x-pack/plugins/beats/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/beats/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -53,12 +53,10 @@ export class KibanaBackendFrameworkAdapter implements BackendFrameworkAdapter { }); } - public registerRoute< - RouteRequest extends FrameworkWrappableRequest, - RouteResponse - >(route: FrameworkRouteOptions) { - const wrappedHandler = (request: any, reply: any) => - route.handler(wrapRequest(request), reply); + public registerRoute( + route: FrameworkRouteOptions + ) { + const wrappedHandler = (request: any, reply: any) => route.handler(wrapRequest(request), reply); this.server.route({ handler: wrappedHandler, diff --git a/x-pack/plugins/beats/server/lib/adapters/framework/testing_framework_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/framework/testing_framework_adapter.ts index 3bffe274fba8d..3280220893892 100644 --- a/x-pack/plugins/beats/server/lib/adapters/framework/testing_framework_adapter.ts +++ b/x-pack/plugins/beats/server/lib/adapters/framework/testing_framework_adapter.ts @@ -49,10 +49,9 @@ export class TestingBackendFrameworkAdapter implements BackendFrameworkAdapter { // not yet testable } - public registerRoute< - RouteRequest extends FrameworkWrappableRequest, - RouteResponse - >(route: FrameworkRouteOptions) { + public registerRoute( + route: FrameworkRouteOptions + ) { // not yet testable } @@ -68,11 +67,7 @@ export class TestingBackendFrameworkAdapter implements BackendFrameworkAdapter { return await api(options); } - public async callWithRequest( - req: FrameworkRequest, - esMethod: string, - options: {} - ) { + public async callWithRequest(req: FrameworkRequest, esMethod: string, options: {}) { const api = get(this.client, esMethod); api(options); diff --git a/x-pack/plugins/beats/server/lib/adapters/tokens/adapter_types.ts b/x-pack/plugins/beats/server/lib/adapters/tokens/adapter_types.ts index 71fb719d9952a..2fe8c811c396e 100644 --- a/x-pack/plugins/beats/server/lib/adapters/tokens/adapter_types.ts +++ b/x-pack/plugins/beats/server/lib/adapters/tokens/adapter_types.ts @@ -13,8 +13,5 @@ export interface TokenEnrollmentData { export interface CMTokensAdapter { deleteEnrollmentToken(enrollmentToken: string): Promise; getEnrollmentToken(enrollmentToken: string): Promise; - upsertTokens( - user: FrameworkUser, - tokens: TokenEnrollmentData[] - ): Promise; + upsertTokens(user: FrameworkUser, tokens: TokenEnrollmentData[]): Promise; } diff --git a/x-pack/plugins/beats/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts index 6dbefce514052..6aa9ceff46629 100644 --- a/x-pack/plugins/beats/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts +++ b/x-pack/plugins/beats/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts @@ -7,10 +7,7 @@ import { flatten, get } from 'lodash'; import { INDEX_NAMES } from '../../../../common/constants'; import { DatabaseAdapter } from '../database/adapter_types'; -import { - BackendFrameworkAdapter, - FrameworkUser, -} from '../framework/adapter_types'; +import { BackendFrameworkAdapter, FrameworkUser } from '../framework/adapter_types'; import { CMTokensAdapter, TokenEnrollmentData } from './adapter_types'; export class ElasticsearchTokensAdapter implements CMTokensAdapter { @@ -32,9 +29,7 @@ export class ElasticsearchTokensAdapter implements CMTokensAdapter { await this.database.delete(this.framework.internalUser, params); } - public async getEnrollmentToken( - tokenString: string - ): Promise { + public async getEnrollmentToken(tokenString: string): Promise { const params = { id: `enrollment_token:${tokenString}`, ignore: [404], @@ -42,18 +37,11 @@ export class ElasticsearchTokensAdapter implements CMTokensAdapter { type: '_doc', }; - const response = await this.database.get( - this.framework.internalUser, - params - ); - const tokenDetails = get( - response, - '_source.enrollment_token', - { - expires_on: '0', - token: null, - } - ); + const response = await this.database.get(this.framework.internalUser, params); + const tokenDetails = get(response, '_source.enrollment_token', { + expires_on: '0', + token: null, + }); // Elasticsearch might return fast if the token is not found. OR it might return fast // if the token *is* found. Either way, an attacker could using a timing attack to figure @@ -65,10 +53,7 @@ export class ElasticsearchTokensAdapter implements CMTokensAdapter { ); } - public async upsertTokens( - user: FrameworkUser, - tokens: TokenEnrollmentData[] - ) { + public async upsertTokens(user: FrameworkUser, tokens: TokenEnrollmentData[]) { const body = flatten( tokens.map(token => [ { index: { _id: `enrollment_token:${token.token}` } }, diff --git a/x-pack/plugins/beats/server/lib/adapters/tokens/memory_tokens_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/tokens/memory_tokens_adapter.ts index 1eed188d3f2e9..7cf132bb6ba31 100644 --- a/x-pack/plugins/beats/server/lib/adapters/tokens/memory_tokens_adapter.ts +++ b/x-pack/plugins/beats/server/lib/adapters/tokens/memory_tokens_adapter.ts @@ -15,31 +15,22 @@ export class MemoryTokensAdapter implements CMTokensAdapter { } public async deleteEnrollmentToken(enrollmentToken: string) { - const index = this.tokenDB.findIndex( - token => token.token === enrollmentToken - ); + const index = this.tokenDB.findIndex(token => token.token === enrollmentToken); if (index > -1) { this.tokenDB.splice(index, 1); } } - public async getEnrollmentToken( - tokenString: string - ): Promise { + public async getEnrollmentToken(tokenString: string): Promise { return new Promise(resolve => { return resolve(this.tokenDB.find(token => token.token === tokenString)); }); } - public async upsertTokens( - user: FrameworkAuthenticatedUser, - tokens: TokenEnrollmentData[] - ) { + public async upsertTokens(user: FrameworkAuthenticatedUser, tokens: TokenEnrollmentData[]) { tokens.forEach(token => { - const existingIndex = this.tokenDB.findIndex( - t => t.token === token.token - ); + const existingIndex = this.tokenDB.findIndex(t => t.token === token.token); if (existingIndex !== -1) { this.tokenDB[existingIndex] = token; } else { diff --git a/x-pack/plugins/beats/server/lib/compose/kibana.ts b/x-pack/plugins/beats/server/lib/compose/kibana.ts index 7c46f82b84bf3..685171669a887 100644 --- a/x-pack/plugins/beats/server/lib/compose/kibana.ts +++ b/x-pack/plugins/beats/server/lib/compose/kibana.ts @@ -5,9 +5,9 @@ */ import { ElasticsearchBeatsAdapter } from '../adapters/beats/elasticsearch_beats_adapter'; +import { KibanaDatabaseAdapter } from '../adapters/database/kibana_database_adapter'; import { ElasticsearchTagsAdapter } from '../adapters/tags/elasticsearch_tags_adapter'; import { ElasticsearchTokensAdapter } from '../adapters/tokens/elasticsearch_tokens_adapter'; -import { KibanaDatabaseAdapter } from './../adapters/database/kibana_database_adapter'; import { KibanaBackendFrameworkAdapter } from '../adapters/framework/kibana_framework_adapter'; @@ -22,19 +22,13 @@ export function compose(server: any): CMServerLibs { const database = new KibanaDatabaseAdapter(server.plugins.elasticsearch); const tags = new CMTagsDomain(new ElasticsearchTagsAdapter(database)); - const tokens = new CMTokensDomain( - new ElasticsearchTokensAdapter(database, framework), - { - framework, - } - ); - const beats = new CMBeatsDomain( - new ElasticsearchBeatsAdapter(database, framework), - { - tags, - tokens, - } - ); + const tokens = new CMTokensDomain(new ElasticsearchTokensAdapter(database, framework), { + framework, + }); + const beats = new CMBeatsDomain(new ElasticsearchBeatsAdapter(database, framework), { + tags, + tokens, + }); const domainLibs: CMDomainLibs = { beats, diff --git a/x-pack/plugins/beats/server/lib/domains/__tests__/beats/assign_tags.test.ts b/x-pack/plugins/beats/server/lib/domains/__tests__/beats/assign_tags.test.ts index f28fcda8004e4..c9f9f15256d43 100644 --- a/x-pack/plugins/beats/server/lib/domains/__tests__/beats/assign_tags.test.ts +++ b/x-pack/plugins/beats/server/lib/domains/__tests__/beats/assign_tags.test.ts @@ -101,9 +101,7 @@ describe('Beats Domain Lib', () => { { beatId: 'bar', tag: 'production' }, ]); - expect(apiResponse.assignments).toEqual([ - { status: 200, result: 'updated' }, - ]); + expect(apiResponse.assignments).toEqual([{ status: 200, result: 'updated' }]); }); it('should not re-add an existing tag to a beat', async () => { @@ -117,9 +115,7 @@ describe('Beats Domain Lib', () => { { beatId: 'foo', tag: 'production' }, ]); - expect(apiResponse.assignments).toEqual([ - { status: 200, result: 'updated' }, - ]); + expect(apiResponse.assignments).toEqual([{ status: 200, result: 'updated' }]); beat = beatsDB.find(b => b.id === 'foo') as any; expect(beat.tags).toEqual([...tags, 'qa']); diff --git a/x-pack/plugins/beats/server/lib/domains/__tests__/beats/enroll.test.ts b/x-pack/plugins/beats/server/lib/domains/__tests__/beats/enroll.test.ts index 1466e9ed926c7..f60c1ed0e009e 100644 --- a/x-pack/plugins/beats/server/lib/domains/__tests__/beats/enroll.test.ts +++ b/x-pack/plugins/beats/server/lib/domains/__tests__/beats/enroll.test.ts @@ -8,6 +8,7 @@ import { MemoryBeatsAdapter } from '../../../adapters/beats/memory_beats_adapter import { TestingBackendFrameworkAdapter } from '../../../adapters/framework/testing_framework_adapter'; import { MemoryTagsAdapter } from '../../../adapters/tags/memory_tags_adapter'; import { MemoryTokensAdapter } from '../../../adapters/tokens/memory_tokens_adapter'; +import { BeatEnrollmentStatus } from '../../../lib'; import { BeatTag, CMBeat } from '../../../../../common/domain_types'; import { TokenEnrollmentData } from '../../../adapters/tokens/adapter_types'; @@ -84,16 +85,16 @@ describe('Beats Domain Lib', () => { }); it('should enroll beat, returning an access token', async () => { - const { token } = await tokensLib.getEnrollmentToken( - validEnrollmentToken - ); + const { token } = await tokensLib.getEnrollmentToken(validEnrollmentToken); expect(token).toEqual(validEnrollmentToken); - const { accessToken } = await beatsLib.enrollBeat( + const { accessToken, status } = await beatsLib.enrollBeat( + validEnrollmentToken, beatId, '192.168.1.1', omit(beat, 'enrollment_token') ); + expect(status).toEqual(BeatEnrollmentStatus.Success); expect(beatsDB.length).toEqual(1); expect(beatsDB[0]).toHaveProperty('host_ip'); @@ -126,9 +127,7 @@ describe('Beats Domain Lib', () => { await tokensLib.deleteEnrollmentToken(validEnrollmentToken); expect(tokensDB.length).toEqual(0); - const { token } = await tokensLib.getEnrollmentToken( - validEnrollmentToken - ); + const { token } = await tokensLib.getEnrollmentToken(validEnrollmentToken); expect(token).toEqual(null); }); diff --git a/x-pack/plugins/beats/server/lib/domains/__tests__/beats/verify.test.ts b/x-pack/plugins/beats/server/lib/domains/__tests__/beats/verify.test.ts deleted file mode 100644 index 140a17917684d..0000000000000 --- a/x-pack/plugins/beats/server/lib/domains/__tests__/beats/verify.test.ts +++ /dev/null @@ -1,183 +0,0 @@ -/* - * 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 { MemoryBeatsAdapter } from '../../../adapters/beats/memory_beats_adapter'; -import { TestingBackendFrameworkAdapter } from '../../../adapters/framework/testing_framework_adapter'; -import { MemoryTagsAdapter } from '../../../adapters/tags/memory_tags_adapter'; -import { MemoryTokensAdapter } from '../../../adapters/tokens/memory_tokens_adapter'; - -import { BeatTag, CMBeat } from '../../../../../common/domain_types'; -import { TokenEnrollmentData } from '../../../adapters/tokens/adapter_types'; - -import { CMBeatsDomain } from '../../beats'; -import { CMTagsDomain } from '../../tags'; -import { CMTokensDomain } from '../../tokens'; - -import Chance from 'chance'; - -const seed = Date.now(); -const chance = new Chance(seed); - -const settings = { - encryptionKey: 'something_who_cares', - enrollmentTokensTtlInSeconds: 10 * 60, // 10 minutes -}; - -describe('Beats Domain Lib', () => { - let beatsLib: CMBeatsDomain; - let tokensLib: CMTokensDomain; - - let beatsDB: CMBeat[] = []; - let tagsDB: BeatTag[] = []; - let tokensDB: TokenEnrollmentData[] = []; - - describe('verify_beat', () => { - beforeEach(async () => { - beatsDB = [ - { - access_token: '9a6c99ae0fd84b068819701169cd8a4b', - host_ip: '1.2.3.4', - host_name: 'foo.bar.com', - id: 'qux', - type: 'filebeat', - }, - { - access_token: '188255eb560a4448b72656c5e99cae6f', - host_ip: '22.33.11.44', - host_name: 'baz.bar.com', - id: 'baz', - type: 'metricbeat', - }, - { - access_token: '93c4a4dd08564c189a7ec4e4f046b975', - host_ip: '1.2.3.4', - host_name: 'foo.bar.com', - id: 'foo', - tags: ['production', 'qa'], - type: 'metricbeat', - verified_on: '2018-05-15T16:25:38.924Z', - }, - { - access_token: '3c4a4dd08564c189a7ec4e4f046b9759', - host_ip: '11.22.33.44', - host_name: 'foo.com', - id: 'bar', - type: 'filebeat', - }, - ]; - tagsDB = [ - { - configuration_blocks: [], - id: 'production', - }, - { - configuration_blocks: [], - id: 'development', - }, - { - configuration_blocks: [], - id: 'qa', - }, - ]; - tokensDB = []; - - const framework = new TestingBackendFrameworkAdapter(null, settings); - - tokensLib = new CMTokensDomain(new MemoryTokensAdapter(tokensDB), { - framework, - }); - - const tagsLib = new CMTagsDomain(new MemoryTagsAdapter(tagsDB)); - - beatsLib = new CMBeatsDomain(new MemoryBeatsAdapter(beatsDB), { - tags: tagsLib, - tokens: tokensLib, - }); - }); - - it('should return errors for non-existent beats', async () => { - const nonExistentBeatId = chance.word(); - - interface Beats { - id: string; - status?: number; - result?: string; - } - - const beats: Beats[] = [{ id: 'bar' }, { id: nonExistentBeatId }]; - const beatIds = beats.map(b => b.id); - - const { - verifiedBeatIds, - alreadyVerifiedBeatIds, - nonExistentBeatIds, - } = await beatsLib.verifyBeats({ kind: 'unauthenticated' }, beatIds); - - // TODO calculation of status should be done in-lib, w/switch statement here - beats.forEach(b => { - if (nonExistentBeatIds.includes(b.id)) { - b.status = 404; - b.result = 'not found'; - } else if (alreadyVerifiedBeatIds.includes(b.id)) { - b.status = 200; - b.result = 'already verified'; - } else if (verifiedBeatIds.includes(b.id)) { - b.status = 200; - b.result = 'verified'; - } else { - b.status = 400; - b.result = 'not verified'; - } - }); - - const response = { beats }; - expect(response.beats).toEqual([ - { id: 'bar', status: 200, result: 'verified' }, - { id: nonExistentBeatId, status: 404, result: 'not found' }, - ]); - }); - - it('should not re-verify already-verified beats', async () => { - interface Beats { - id: string; - status?: number; - result?: string; - } - - const beats: Beats[] = [{ id: 'foo' }, { id: 'bar' }]; - const beatIds = beats.map(b => b.id); - - const { - verifiedBeatIds, - alreadyVerifiedBeatIds, - nonExistentBeatIds, - } = await beatsLib.verifyBeats({ kind: 'unauthenticated' }, beatIds); - - // TODO calculation of status should be done in-lib, w/switch statement here - beats.forEach(beat => { - if (nonExistentBeatIds.includes(beat.id)) { - beat.status = 404; - beat.result = 'not found'; - } else if (alreadyVerifiedBeatIds.includes(beat.id)) { - beat.status = 200; - beat.result = 'already verified'; - } else if (verifiedBeatIds.includes(beat.id)) { - beat.status = 200; - beat.result = 'verified'; - } else { - beat.status = 400; - beat.result = 'not verified'; - } - }); - - const response = { beats }; - expect(response.beats).toEqual([ - { id: 'foo', status: 200, result: 'already verified' }, - { id: 'bar', status: 200, result: 'verified' }, - ]); - }); - }); -}); diff --git a/x-pack/plugins/beats/server/lib/domains/__tests__/tokens.test.ts b/x-pack/plugins/beats/server/lib/domains/__tests__/tokens.test.ts index 8002013fa4795..c89962dca7d3b 100644 --- a/x-pack/plugins/beats/server/lib/domains/__tests__/tokens.test.ts +++ b/x-pack/plugins/beats/server/lib/domains/__tests__/tokens.test.ts @@ -36,10 +36,7 @@ describe('Token Domain Lib', () => { }); it('should generate webtokens with a qty of 1', async () => { - const tokens = await tokensLib.createEnrollmentTokens( - framework.internalUser, - 1 - ); + const tokens = await tokensLib.createEnrollmentTokens(framework.internalUser, 1); expect(tokens.length).toBe(1); @@ -48,15 +45,10 @@ describe('Token Domain Lib', () => { it('should create the specified number of tokens', async () => { const numTokens = chance.integer({ min: 1, max: 20 }); - const tokensFromApi = await tokensLib.createEnrollmentTokens( - framework.internalUser, - numTokens - ); + const tokensFromApi = await tokensLib.createEnrollmentTokens(framework.internalUser, numTokens); expect(tokensFromApi.length).toEqual(numTokens); - expect(tokensFromApi).toEqual( - tokensDB.map((t: TokenEnrollmentData) => t.token) - ); + expect(tokensFromApi).toEqual(tokensDB.map((t: TokenEnrollmentData) => t.token)); }); it('should set token expiration to 10 minutes from now by default', async () => { diff --git a/x-pack/plugins/beats/server/lib/domains/beats.ts b/x-pack/plugins/beats/server/lib/domains/beats.ts index 9a580e2291ee7..07a084e54b53d 100644 --- a/x-pack/plugins/beats/server/lib/domains/beats.ts +++ b/x-pack/plugins/beats/server/lib/domains/beats.ts @@ -5,17 +5,15 @@ */ import { uniq } from 'lodash'; +import moment from 'moment'; import { findNonExistentItems } from '../../utils/find_non_existent_items'; import { CMBeat } from '../../../common/domain_types'; -import { - BeatsTagAssignment, - CMBeatsAdapter, -} from '../adapters/beats/adapter_types'; +import { BeatsTagAssignment, CMBeatsAdapter } from '../adapters/beats/adapter_types'; import { FrameworkUser } from '../adapters/framework/adapter_types'; import { CMAssignmentReturn } from '../adapters/beats/adapter_types'; -import { CMDomainLibs } from '../lib'; +import { BeatEnrollmentStatus, CMDomainLibs } from '../lib'; import { BeatsRemovalReturn } from './../adapters/beats/adapter_types'; export class CMBeatsDomain { @@ -32,11 +30,11 @@ export class CMBeatsDomain { this.tokens = libs.tokens; } - public async update( - beatId: string, - accessToken: string, - beatData: Partial - ) { + public async getById(beatId: string) { + return await this.adapter.get(beatId); + } + + public async update(beatId: string, accessToken: string, beatData: Partial) { const beat = await this.adapter.get(beatId); const { verified: isAccessTokenValid } = this.tokens.verifyToken( @@ -52,10 +50,6 @@ export class CMBeatsDomain { if (!isAccessTokenValid) { return 'invalid-access-token'; } - const isBeatVerified = beat.hasOwnProperty('verified_on'); - if (!isBeatVerified) { - return 'beat-not-verified'; - } await this.adapter.update({ ...beat, @@ -65,18 +59,34 @@ export class CMBeatsDomain { // TODO more strongly type this public async enrollBeat( + enrollmentToken: string, beatId: string, remoteAddress: string, beat: Partial - ) { + ): Promise<{ status: string; accessToken?: string }> { + const { token, expires_on } = await this.tokens.getEnrollmentToken(enrollmentToken); + + if (expires_on && moment(expires_on).isBefore(moment())) { + return { status: BeatEnrollmentStatus.ExpiredEnrollmentToken }; + } + if (!token) { + return { status: BeatEnrollmentStatus.InvalidEnrollmentToken }; + } + const accessToken = this.tokens.generateAccessToken(); + const verifiedOn = moment().toJSON(); + await this.adapter.insert({ ...beat, + verified_on: verifiedOn, access_token: accessToken, host_ip: remoteAddress, id: beatId, } as CMBeat); - return { accessToken }; + + await this.tokens.deleteEnrollmentToken(enrollmentToken); + + return { status: BeatEnrollmentStatus.Success, accessToken }; } public async removeTagsFromBeats( @@ -115,10 +125,7 @@ export class CMBeatsDomain { .filter((removal, idx) => response.removals[idx].status === null); if (validRemovals.length > 0) { - const removalResults = await this.adapter.removeTagsFromBeats( - user, - validRemovals - ); + const removalResults = await this.adapter.removeTagsFromBeats(user, validRemovals); return addToResultsToResponse('removals', response, removalResults); } return response; @@ -128,33 +135,6 @@ export class CMBeatsDomain { return await this.adapter.getAll(user); } - // TODO cleanup return value, should return a status enum - public async verifyBeats(user: FrameworkUser, beatIds: string[]) { - const beatsFromEs = await this.adapter.getWithIds(user, beatIds); - - const nonExistentBeatIds = findNonExistentItems(beatsFromEs, beatIds); - - const alreadyVerifiedBeatIds = beatsFromEs - .filter((beat: any) => beat.hasOwnProperty('verified_on')) - .map((beat: any) => beat.id); - - const toBeVerifiedBeatIds = beatsFromEs - .filter((beat: any) => !beat.hasOwnProperty('verified_on')) - .map((beat: any) => beat.id); - - const verifications = await this.adapter.verifyBeats( - user, - toBeVerifiedBeatIds - ); - - return { - alreadyVerifiedBeatIds, - nonExistentBeatIds, - toBeVerifiedBeatIds, - verifiedBeatIds: verifications.map((v: any) => v.id), - }; - } - public async assignTagsToBeats( user: FrameworkUser, assignments: BeatsTagAssignment[] @@ -191,10 +171,7 @@ export class CMBeatsDomain { .filter((assignment, idx) => response.assignments[idx].status === null); if (validAssignments.length > 0) { - const assignmentResults = await this.adapter.assignTagsToBeats( - user, - validAssignments - ); + const assignmentResults = await this.adapter.assignTagsToBeats(user, validAssignments); // TODO This should prob not mutate return addToResultsToResponse('assignments', response, assignmentResults); @@ -229,11 +206,7 @@ function addNonExistentItemToResponse( } // TODO dont mutate response -function addToResultsToResponse( - key: string, - response: any, - assignmentResults: any -) { +function addToResultsToResponse(key: string, response: any, assignmentResults: any) { assignmentResults.forEach((assignmentResult: any) => { const { idxInRequest, status, result } = assignmentResult; response[key][idxInRequest].status = status; diff --git a/x-pack/plugins/beats/server/lib/domains/tags.ts b/x-pack/plugins/beats/server/lib/domains/tags.ts index 194551721ab33..b6ce9f7e14821 100644 --- a/x-pack/plugins/beats/server/lib/domains/tags.ts +++ b/x-pack/plugins/beats/server/lib/domains/tags.ts @@ -22,14 +22,8 @@ export class CMTagsDomain { return await this.adapter.getTagsWithIds(user, tagIds); } - public async saveTag( - user: FrameworkUser, - tagId: string, - configs: ConfigurationBlock[] - ) { - const { isValid, message } = await this.validateConfigurationBlocks( - configs - ); + public async saveTag(user: FrameworkUser, tagId: string, configs: ConfigurationBlock[]) { + const { isValid, message } = await this.validateConfigurationBlocks(configs); if (!isValid) { return { isValid, result: message }; } @@ -49,10 +43,7 @@ export class CMTagsDomain { // If none of the types in the given configuration blocks are uniqueness-enforcing, // we don't need to perform any further validation checks. - const uniquenessEnforcingTypes = intersection( - types, - UNIQUENESS_ENFORCING_TYPES - ); + const uniquenessEnforcingTypes = intersection(types, UNIQUENESS_ENFORCING_TYPES); if (uniquenessEnforcingTypes.length === 0) { return { isValid: true }; } diff --git a/x-pack/plugins/beats/server/lib/domains/tokens.ts b/x-pack/plugins/beats/server/lib/domains/tokens.ts index 6cfb922c34d57..dc7ffa9a63356 100644 --- a/x-pack/plugins/beats/server/lib/domains/tokens.ts +++ b/x-pack/plugins/beats/server/lib/domains/tokens.ts @@ -18,10 +18,7 @@ export class CMTokensDomain { private adapter: CMTokensAdapter; private framework: BackendFrameworkAdapter; - constructor( - adapter: CMTokensAdapter, - libs: { framework: BackendFrameworkAdapter } - ) { + constructor(adapter: CMTokensAdapter, libs: { framework: BackendFrameworkAdapter }) { this.adapter = adapter; this.framework = libs.framework; } @@ -37,11 +34,7 @@ export class CMTokensDomain { }; } - const { verified, expired } = this.verifyToken( - enrollmentToken, - fullToken.token || '', - false - ); + const { verified, expired } = this.verifyToken(enrollmentToken, fullToken.token || '', false); if (!verified) { return { @@ -63,9 +56,7 @@ export class CMTokensDomain { let expired = false; if (decode) { - const enrollmentTokenSecret = this.framework.getSetting( - 'xpack.beats.encryptionKey' - ); + const enrollmentTokenSecret = this.framework.getSetting('xpack.beats.encryptionKey'); try { verifyToken(recivedToken, enrollmentTokenSecret); @@ -99,17 +90,13 @@ export class CMTokensDomain { return { expired, verified: - timingSafeEqual( - Buffer.from(recivedToken, 'utf8'), - Buffer.from(token2, 'utf8') - ) && tokenDecoded, + timingSafeEqual(Buffer.from(recivedToken, 'utf8'), Buffer.from(token2, 'utf8')) && + tokenDecoded, }; } public generateAccessToken() { - const enrollmentTokenSecret = this.framework.getSetting( - 'xpack.beats.encryptionKey' - ); + const enrollmentTokenSecret = this.framework.getSetting('xpack.beats.encryptionKey'); const tokenData = { created: moment().toJSON(), diff --git a/x-pack/plugins/beats/server/lib/lib.ts b/x-pack/plugins/beats/server/lib/lib.ts index d916c18aa4e4a..495093842b496 100644 --- a/x-pack/plugins/beats/server/lib/lib.ts +++ b/x-pack/plugins/beats/server/lib/lib.ts @@ -20,3 +20,9 @@ export interface CMServerLibs extends CMDomainLibs { framework: BackendFrameworkAdapter; database: DatabaseAdapter; } + +export enum BeatEnrollmentStatus { + Success = 'Success', + ExpiredEnrollmentToken = 'Expired enrollment token', + InvalidEnrollmentToken = 'Invalid enrollment token', +} diff --git a/x-pack/plugins/beats/server/management_server.ts b/x-pack/plugins/beats/server/management_server.ts index 637da2e37bd07..1c8a1d727175e 100644 --- a/x-pack/plugins/beats/server/management_server.ts +++ b/x-pack/plugins/beats/server/management_server.ts @@ -5,15 +5,14 @@ */ import { CMServerLibs } from './lib/lib'; +import { createGetBeatConfigurationRoute } from './rest_api/beats/configuration'; import { createBeatEnrollmentRoute } from './rest_api/beats/enroll'; import { createListAgentsRoute } from './rest_api/beats/list'; import { createTagAssignmentsRoute } from './rest_api/beats/tag_assignment'; import { createTagRemovalsRoute } from './rest_api/beats/tag_removal'; import { createBeatUpdateRoute } from './rest_api/beats/update'; -import { createBeatVerificationRoute } from './rest_api/beats/verify'; import { createSetTagRoute } from './rest_api/tags/set'; import { createTokensRoute } from './rest_api/tokens/create'; - import { beatsIndexTemplate } from './utils/index_templates'; export const initManagementServer = (libs: CMServerLibs) => { @@ -22,12 +21,12 @@ export const initManagementServer = (libs: CMServerLibs) => { body: beatsIndexTemplate, }); + libs.framework.registerRoute(createGetBeatConfigurationRoute(libs)); libs.framework.registerRoute(createTagAssignmentsRoute(libs)); libs.framework.registerRoute(createListAgentsRoute(libs)); libs.framework.registerRoute(createTagRemovalsRoute(libs)); libs.framework.registerRoute(createBeatEnrollmentRoute(libs)); libs.framework.registerRoute(createSetTagRoute(libs)); libs.framework.registerRoute(createTokensRoute(libs)); - libs.framework.registerRoute(createBeatVerificationRoute(libs)); libs.framework.registerRoute(createBeatUpdateRoute(libs)); }; diff --git a/x-pack/plugins/beats/server/rest_api/beats/configuration.ts b/x-pack/plugins/beats/server/rest_api/beats/configuration.ts new file mode 100644 index 0000000000000..81906ea9473e6 --- /dev/null +++ b/x-pack/plugins/beats/server/rest_api/beats/configuration.ts @@ -0,0 +1,54 @@ +/* + * 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 { BeatTag, ConfigurationBlock } from '../../../common/domain_types'; +import { CMServerLibs } from '../../lib/lib'; +import { wrapEsError } from '../../utils/error_wrappers'; + +export const createGetBeatConfigurationRoute = (libs: CMServerLibs) => ({ + method: 'GET', + path: '/api/beats/agent/{beatId}/configuration', + config: { + validate: { + headers: Joi.object({ + 'kbn-beats-access-token': Joi.string().required(), + }).options({ allowUnknown: true }), + }, + auth: false, + }, + handler: async (request: any, reply: any) => { + const beatId = request.params.beatId; + const accessToken = request.headers['kbn-beats-access-token']; + + let beat; + let tags; + try { + beat = await libs.beats.getById(beatId); + if (beat === null) { + return reply({ message: 'Beat not found' }).code(404); + } + + const isAccessTokenValid = beat.access_token === accessToken; + if (!isAccessTokenValid) { + return reply({ message: 'Invalid access token' }).code(401); + } + + tags = await libs.tags.getTagsWithIds(libs.framework.internalUser, beat.tags || []); + } catch (err) { + return reply(wrapEsError(err)); + } + + const configurationBlocks = tags.reduce((blocks: ConfigurationBlock[], tag: BeatTag) => { + blocks = blocks.concat(tag.configuration_blocks); + return blocks; + }, []); + + reply({ + configuration_blocks: configurationBlocks, + }); + }, +}); diff --git a/x-pack/plugins/beats/server/rest_api/beats/enroll.ts b/x-pack/plugins/beats/server/rest_api/beats/enroll.ts index d909b64810360..c5b01fdbd4cc3 100644 --- a/x-pack/plugins/beats/server/rest_api/beats/enroll.ts +++ b/x-pack/plugins/beats/server/rest_api/beats/enroll.ts @@ -3,13 +3,12 @@ * 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 { omit } from 'lodash'; -import moment from 'moment'; import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; import { CMServerLibs } from '../../lib/lib'; import { wrapEsError } from '../../utils/error_wrappers'; +import { BeatEnrollmentStatus } from './../../lib/lib'; // TODO: add license check pre-hook // TODO: write to Kibana audit log file @@ -34,25 +33,26 @@ export const createBeatEnrollmentRoute = (libs: CMServerLibs) => ({ const enrollmentToken = request.headers['kbn-beats-enrollment-token']; try { - const { token, expires_on } = await libs.tokens.getEnrollmentToken( - enrollmentToken - ); - - if (expires_on && moment(expires_on).isBefore(moment())) { - return reply({ message: 'Expired enrollment token' }).code(400); - } - if (!token) { - return reply({ message: 'Invalid enrollment token' }).code(400); - } - const { accessToken } = await libs.beats.enrollBeat( + const { status, accessToken } = await libs.beats.enrollBeat( + enrollmentToken, beatId, request.info.remoteAddress, omit(request.payload, 'enrollment_token') ); - await libs.tokens.deleteEnrollmentToken(enrollmentToken); - - reply({ access_token: accessToken }).code(201); + switch (status) { + case BeatEnrollmentStatus.ExpiredEnrollmentToken: + return reply({ + message: BeatEnrollmentStatus.ExpiredEnrollmentToken, + }).code(400); + case BeatEnrollmentStatus.InvalidEnrollmentToken: + return reply({ + message: BeatEnrollmentStatus.InvalidEnrollmentToken, + }).code(400); + case BeatEnrollmentStatus.Success: + default: + return reply({ access_token: accessToken }).code(201); + } } catch (err) { // TODO move this to kibana route thing in adapter return reply(wrapEsError(err)); diff --git a/x-pack/plugins/beats/server/rest_api/beats/tag_assignment.ts b/x-pack/plugins/beats/server/rest_api/beats/tag_assignment.ts index a6654250d196f..9c832ee3226b8 100644 --- a/x-pack/plugins/beats/server/rest_api/beats/tag_assignment.ts +++ b/x-pack/plugins/beats/server/rest_api/beats/tag_assignment.ts @@ -34,10 +34,7 @@ export const createTagAssignmentsRoute = (libs: CMServerLibs) => ({ })); try { - const response = await libs.beats.assignTagsToBeats( - request.user, - tweakedAssignments - ); + const response = await libs.beats.assignTagsToBeats(request.user, tweakedAssignments); reply(response); } catch (err) { // TODO move this to kibana route thing in adapter diff --git a/x-pack/plugins/beats/server/rest_api/beats/tag_removal.ts b/x-pack/plugins/beats/server/rest_api/beats/tag_removal.ts index 1b495b906a364..6a7af3b42d407 100644 --- a/x-pack/plugins/beats/server/rest_api/beats/tag_removal.ts +++ b/x-pack/plugins/beats/server/rest_api/beats/tag_removal.ts @@ -34,10 +34,7 @@ export const createTagRemovalsRoute = (libs: CMServerLibs) => ({ })); try { - const response = await libs.beats.removeTagsFromBeats( - request.user, - tweakedRemovals - ); + const response = await libs.beats.removeTagsFromBeats(request.user, tweakedRemovals); reply(response); } catch (err) { // TODO move this to kibana route thing in adapter diff --git a/x-pack/plugins/beats/server/rest_api/beats/update.ts b/x-pack/plugins/beats/server/rest_api/beats/update.ts index c86cf74b0c744..60d7151731cd3 100644 --- a/x-pack/plugins/beats/server/rest_api/beats/update.ts +++ b/x-pack/plugins/beats/server/rest_api/beats/update.ts @@ -49,8 +49,6 @@ export const createBeatUpdateRoute = (libs: CMServerLibs) => ({ return reply({ message: 'Beat not found' }).code(404); case 'invalid-access-token': return reply({ message: 'Invalid access token' }).code(401); - case 'beat-not-verified': - return reply({ message: 'Beat has not been verified' }).code(400); } reply().code(204); diff --git a/x-pack/plugins/beats/server/rest_api/beats/verify.ts b/x-pack/plugins/beats/server/rest_api/beats/verify.ts deleted file mode 100644 index d15b0374f445f..0000000000000 --- a/x-pack/plugins/beats/server/rest_api/beats/verify.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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 { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; -import { CMServerLibs } from '../../lib/lib'; -import { wrapEsError } from '../../utils/error_wrappers'; - -// TODO: add license check pre-hook -// TODO: write to Kibana audit log file -export const createBeatVerificationRoute = (libs: CMServerLibs) => ({ - config: { - auth: false, - validate: { - payload: Joi.object({ - beats: Joi.array() - .items({ - id: Joi.string().required(), - }) - .min(1), - }).required(), - }, - }, - handler: async (request: FrameworkRequest, reply: any) => { - const beats = [...request.payload.beats]; - const beatIds = beats.map(beat => beat.id); - - try { - const { - verifiedBeatIds, - alreadyVerifiedBeatIds, - nonExistentBeatIds, - } = await libs.beats.verifyBeats(request.user, beatIds); - - // TODO calculation of status should be done in-lib, w/switch statement here - beats.forEach(beat => { - if (nonExistentBeatIds.includes(beat.id)) { - beat.status = 404; - beat.result = 'not found'; - } else if (alreadyVerifiedBeatIds.includes(beat.id)) { - beat.status = 200; - beat.result = 'already verified'; - } else if (verifiedBeatIds.includes(beat.id)) { - beat.status = 200; - beat.result = 'verified'; - } else { - beat.status = 400; - beat.result = 'not verified'; - } - }); - - const response = { beats }; - reply(response); - } catch (err) { - return reply(wrapEsError(err)); - } - }, - method: 'POST', - path: '/api/beats/agents/verify', -}); diff --git a/x-pack/plugins/beats/server/rest_api/tags/set.ts b/x-pack/plugins/beats/server/rest_api/tags/set.ts index bdc49ce62a9a9..6c01959e75311 100644 --- a/x-pack/plugins/beats/server/rest_api/tags/set.ts +++ b/x-pack/plugins/beats/server/rest_api/tags/set.ts @@ -32,11 +32,7 @@ export const createSetTagRoute = (libs: CMServerLibs) => ({ }, }, handler: async (request: FrameworkRequest, reply: any) => { - const configurationBlocks = get( - request, - 'payload.configuration_blocks', - [] - ); + const configurationBlocks = get(request, 'payload.configuration_blocks', []); try { const { isValid, result } = await libs.tags.saveTag( request.user, diff --git a/x-pack/plugins/beats/server/rest_api/tokens/create.ts b/x-pack/plugins/beats/server/rest_api/tokens/create.ts index 9e20735d640e5..74278703347c3 100644 --- a/x-pack/plugins/beats/server/rest_api/tokens/create.ts +++ b/x-pack/plugins/beats/server/rest_api/tokens/create.ts @@ -27,10 +27,7 @@ export const createTokensRoute = (libs: CMServerLibs) => ({ const numTokens = get(request, 'payload.num_tokens', DEFAULT_NUM_TOKENS); try { - const tokens = await libs.tokens.createEnrollmentTokens( - request.user, - numTokens - ); + const tokens = await libs.tokens.createEnrollmentTokens(request.user, numTokens); reply({ tokens }); } catch (err) { // TODO move this to kibana route thing in adapter diff --git a/x-pack/plugins/beats/server/utils/error_wrappers/wrap_es_error.test.ts b/x-pack/plugins/beats/server/utils/error_wrappers/wrap_es_error.test.ts index 5087bf3224c42..e7fb1c2adda21 100644 --- a/x-pack/plugins/beats/server/utils/error_wrappers/wrap_es_error.test.ts +++ b/x-pack/plugins/beats/server/utils/error_wrappers/wrap_es_error.test.ts @@ -24,9 +24,7 @@ describe('wrap_es_error', () => { const wrappedError = wrapEsError(originalError); expect(wrappedError.output.statusCode).toEqual(originalError.statusCode); - expect(wrappedError.output.payload.message).toEqual( - originalError.message - ); + expect(wrappedError.output.payload.message).toEqual(originalError.message); }); it('should return invalid permissions message for 403 errors', () => { diff --git a/x-pack/plugins/beats/server/utils/error_wrappers/wrap_es_error.ts b/x-pack/plugins/beats/server/utils/error_wrappers/wrap_es_error.ts index 50ffbcb4a10c9..30328f2c6a833 100644 --- a/x-pack/plugins/beats/server/utils/error_wrappers/wrap_es_error.ts +++ b/x-pack/plugins/beats/server/utils/error_wrappers/wrap_es_error.ts @@ -16,9 +16,7 @@ import Boom from 'boom'; export function wrapEsError(err: any) { const statusCode = err.statusCode; if (statusCode === 403) { - return Boom.forbidden( - 'Insufficient user permissions for managing Beats configuration' - ); + return Boom.forbidden('Insufficient user permissions for managing Beats configuration'); } return Boom.wrap(err, err.statusCode); } diff --git a/x-pack/plugins/beats/server/utils/find_non_existent_items.ts b/x-pack/plugins/beats/server/utils/find_non_existent_items.ts index d6b2a0c9e143b..0e9b4f0b6fa5e 100644 --- a/x-pack/plugins/beats/server/utils/find_non_existent_items.ts +++ b/x-pack/plugins/beats/server/utils/find_non_existent_items.ts @@ -10,17 +10,10 @@ interface RandomItem { } export function findNonExistentItems(items: RandomItem[], requestedItems: any) { - return requestedItems.reduce( - (nonExistentItems: string[], requestedItem: string, idx: number) => { - if ( - items.findIndex( - (item: RandomItem) => item && item.id === requestedItem - ) === -1 - ) { - nonExistentItems.push(requestedItems[idx]); - } - return nonExistentItems; - }, - [] - ); + return requestedItems.reduce((nonExistentItems: string[], requestedItem: string, idx: number) => { + if (items.findIndex((item: RandomItem) => item && item.id === requestedItem) === -1) { + nonExistentItems.push(requestedItems[idx]); + } + return nonExistentItems; + }, []); } diff --git a/x-pack/plugins/beats/wallaby.js b/x-pack/plugins/beats/wallaby.js index 79b1438a5374d..8c0c4aa355925 100644 --- a/x-pack/plugins/beats/wallaby.js +++ b/x-pack/plugins/beats/wallaby.js @@ -4,14 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ const path = require('path'); -process.env.NODE_PATH += - path.delimiter + path.join(__dirname, '..', '..', '..', 'node_modules'); +process.env.NODE_PATH = path.join(__dirname, '..', '..', 'node_modules'); module.exports = function (wallaby) { return { - hints: { - commentAutoLog: 'testOutputWith:', - }, debug: true, files: [ './tsconfig.json', @@ -43,7 +39,6 @@ module.exports = function (wallaby) { '..', '..' ); - wallaby.testFramework.configure({ rootDir: wallaby.localProjectDir, moduleNameMapper: { @@ -61,6 +56,7 @@ module.exports = function (wallaby) { ], transform: { '^.+\\.js$': `${kibanaDirectory}/src/dev/jest/babel_transform.js`, + //"^.+\\.tsx?$": `${kibanaDirectory}/src/dev/jest/ts_transform.js`, }, }); }, diff --git a/x-pack/plugins/logstash/server/lib/error_wrappers/__tests__/wrap_es_error.js b/x-pack/plugins/logstash/server/lib/error_wrappers/__tests__/wrap_es_error.js deleted file mode 100644 index f1b956bdcc3bb..0000000000000 --- a/x-pack/plugins/logstash/server/lib/error_wrappers/__tests__/wrap_es_error.js +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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 { wrapEsError } from '../wrap_es_error'; - -describe('wrap_es_error', () => { - describe('#wrapEsError', () => { - - let originalError; - beforeEach(() => { - originalError = new Error('I am an error'); - originalError.statusCode = 404; - }); - - it('should return a Boom object', () => { - const wrappedError = wrapEsError(originalError); - - expect(wrappedError.isBoom).to.be(true); - }); - - it('should return the correct Boom object', () => { - const wrappedError = wrapEsError(originalError); - - expect(wrappedError.output.statusCode).to.be(originalError.statusCode); - expect(wrappedError.output.payload.message).to.be(originalError.message); - }); - - it('should return invalid permissions message for 403 errors', () => { - const securityError = new Error('I am an error'); - securityError.statusCode = 403; - const wrappedError = wrapEsError(securityError); - - expect(wrappedError.isBoom).to.be(true); - expect(wrappedError.message).to.be('Insufficient user permissions for managing Logstash pipelines'); - }); - }); -}); diff --git a/x-pack/test/api_integration/apis/beats/enroll_beat.js b/x-pack/test/api_integration/apis/beats/enroll_beat.js index 1dde64c9ee1d8..0234359b4f9ff 100644 --- a/x-pack/test/api_integration/apis/beats/enroll_beat.js +++ b/x-pack/test/api_integration/apis/beats/enroll_beat.js @@ -66,7 +66,7 @@ export default function ({ getService }) { id: `beat:${beatId}`, }); - expect(esResponse._source.beat).to.not.have.property('verified_on'); + expect(esResponse._source.beat).to.have.property('verified_on'); expect(esResponse._source.beat).to.have.property('host_ip'); }); diff --git a/x-pack/test/api_integration/apis/beats/get_beat.js b/x-pack/test/api_integration/apis/beats/get_beat.js new file mode 100644 index 0000000000000..8d6a81d100eb4 --- /dev/null +++ b/x-pack/test/api_integration/apis/beats/get_beat.js @@ -0,0 +1,51 @@ +/* + * 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'; + +export default function ({ getService }) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('get_beat_configuration', () => { + const archive = 'beats/list'; + + beforeEach('load beats archive', () => esArchiver.load(archive)); + afterEach('unload beats archive', () => esArchiver.unload(archive)); + + it('should return merged configuration for the beat', async () => { + const { body: apiResponse } = await supertest + .get('/api/beats/agent/foo/configuration') + .set( + 'kbn-beats-access-token', + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.' + + 'eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1fQ.' + + 'SSsX2Byyo1B1bGxV8C3G4QldhE5iH87EY_1r21-bwbI' + ) + .expect(200); + + const configurationBlocks = apiResponse.configuration_blocks; + + expect(configurationBlocks).to.be.an(Array); + expect(configurationBlocks.length).to.be(3); + + expect(configurationBlocks[0].type).to.be('output'); + expect(configurationBlocks[0].block_yml).to.be( + 'elasticsearch:\n hosts: ["localhost:9200"]\n username: "..."' + ); + + expect(configurationBlocks[1].type).to.be('metricbeat.modules'); + expect(configurationBlocks[1].block_yml).to.be( + 'module: memcached\nhosts: ["localhost:11211"]' + ); + + expect(configurationBlocks[2].type).to.be('metricbeat.modules'); + expect(configurationBlocks[2].block_yml).to.be( + 'module: munin\nhosts: ["localhost:4949"]\nnode.namespace: node' + ); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/beats/index.js b/x-pack/test/api_integration/apis/beats/index.js index f8956d3e498ba..a6552a383dbd8 100644 --- a/x-pack/test/api_integration/apis/beats/index.js +++ b/x-pack/test/api_integration/apis/beats/index.js @@ -12,7 +12,7 @@ export default function ({ getService, loadTestFile }) { describe('beats', () => { const cleanup = () => es.indices.delete({ index: ES_INDEX_NAME, - ignore: [ 404 ] + ignore: [404] }); beforeEach(cleanup); @@ -20,10 +20,10 @@ export default function ({ getService, loadTestFile }) { loadTestFile(require.resolve('./create_enrollment_tokens')); loadTestFile(require.resolve('./enroll_beat')); loadTestFile(require.resolve('./list_beats')); - loadTestFile(require.resolve('./verify_beats')); loadTestFile(require.resolve('./update_beat')); loadTestFile(require.resolve('./set_tag')); loadTestFile(require.resolve('./assign_tags_to_beats')); loadTestFile(require.resolve('./remove_tags_from_beats')); + loadTestFile(require.resolve('./get_beat')); }); } diff --git a/x-pack/test/api_integration/apis/beats/update_beat.js b/x-pack/test/api_integration/apis/beats/update_beat.js index fb30970d3cc7f..697e644cb2221 100644 --- a/x-pack/test/api_integration/apis/beats/update_beat.js +++ b/x-pack/test/api_integration/apis/beats/update_beat.js @@ -103,36 +103,6 @@ export default function ({ getService }) { expect(beatInEs._source.beat.ephemeral_id).to.not.be(beat.ephemeral_id); }); - it('should return an error for an existing but unverified beat', async () => { - const beatId = 'bar'; - - const { body } = await supertest - .put(`/api/beats/agent/${beatId}`) - .set('kbn-xsrf', 'xxx') - .set( - 'kbn-beats-access-token', - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.' + - 'eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1fQ.' + - 'SSsX2Byyo1B1bGxV8C3G4QldhE5iH87EY_1r21-bwbI' - ) - .send(beat) - .expect(400); - - expect(body.message).to.be('Beat has not been verified'); - - const beatInEs = await es.get({ - index: ES_INDEX_NAME, - type: ES_TYPE_NAME, - id: `beat:${beatId}`, - }); - - expect(beatInEs._source.beat.id).to.be(beatId); - expect(beatInEs._source.beat.type).to.not.be(beat.type); - expect(beatInEs._source.beat.host_name).to.not.be(beat.host_name); - expect(beatInEs._source.beat.version).to.not.be(beat.version); - expect(beatInEs._source.beat.ephemeral_id).to.not.be(beat.ephemeral_id); - }); - it('should return an error for a non-existent beat', async () => { const beatId = chance.word(); const { body } = await supertest diff --git a/x-pack/test/api_integration/apis/beats/verify_beats.js b/x-pack/test/api_integration/apis/beats/verify_beats.js deleted file mode 100644 index 30521d0483c2d..0000000000000 --- a/x-pack/test/api_integration/apis/beats/verify_beats.js +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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'; - -export default function ({ getService }) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - const chance = getService('chance'); - - describe('verify_beats', () => { - const archive = 'beats/list'; - - beforeEach('load beats archive', () => esArchiver.load(archive)); - afterEach('unload beats archive', () => esArchiver.unload(archive)); - - it('verify the given beats', async () => { - const { body: apiResponse } = await supertest - .post('/api/beats/agents/verify') - .set('kbn-xsrf', 'xxx') - .send({ - beats: [{ id: 'bar' }, { id: 'baz' }], - }) - .expect(200); - - expect(apiResponse.beats).to.eql([ - { id: 'bar', status: 200, result: 'verified' }, - { id: 'baz', status: 200, result: 'verified' }, - ]); - }); - - it('should not re-verify already-verified beats', async () => { - const { body: apiResponse } = await supertest - .post('/api/beats/agents/verify') - .set('kbn-xsrf', 'xxx') - .send({ - beats: [{ id: 'foo' }, { id: 'bar' }], - }) - .expect(200); - - expect(apiResponse.beats).to.eql([ - { id: 'foo', status: 200, result: 'already verified' }, - { id: 'bar', status: 200, result: 'verified' }, - ]); - }); - - it('should return errors for non-existent beats', async () => { - const nonExistentBeatId = chance.word(); - const { body: apiResponse } = await supertest - .post('/api/beats/agents/verify') - .set('kbn-xsrf', 'xxx') - .send({ - beats: [{ id: 'bar' }, { id: nonExistentBeatId }], - }) - .expect(200); - - expect(apiResponse.beats).to.eql([ - { id: 'bar', status: 200, result: 'verified' }, - { id: nonExistentBeatId, status: 404, result: 'not found' }, - ]); - }); - }); -} diff --git a/x-pack/test/functional/es_archives/beats/list/data.json b/x-pack/test/functional/es_archives/beats/list/data.json new file mode 100644 index 0000000000000..bbe5bc8dbe3e5 --- /dev/null +++ b/x-pack/test/functional/es_archives/beats/list/data.json @@ -0,0 +1,139 @@ +{ + "type": "doc", + "value": { + "index": ".management-beats", + "type": "_doc", + "id": "beat:qux", + "source": { + "type": "beat", + "beat": { + "type": "filebeat", + "host_ip": "1.2.3.4", + "host_name": "foo.bar.com", + "id": "qux", + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1fQ.SSsX2Byyo1B1bGxV8C3G4QldhE5iH87EY_1r21-bwbI" + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".management-beats", + "type": "_doc", + "id": "beat:baz", + "source": { + "type": "beat", + "beat": { + "type": "metricbeat", + "host_ip": "22.33.11.44", + "host_name": "baz.bar.com", + "id": "baz", + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1fQ.SSsX2Byyo1B1bGxV8C3G4QldhE5iH87EY_1r21-bwbI" + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".management-beats", + "type": "_doc", + "id": "beat:foo", + "source": { + "type": "beat", + "beat": { + "type": "metricbeat", + "host_ip": "1.2.3.4", + "host_name": "foo.bar.com", + "id": "foo", + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1fQ.SSsX2Byyo1B1bGxV8C3G4QldhE5iH87EY_1r21-bwbI", + "verified_on": "2018-05-15T16:25:38.924Z", + "tags": [ + "production", + "qa" + ] + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".management-beats", + "type": "_doc", + "id": "beat:bar", + "source": { + "type": "beat", + "beat": { + "type": "filebeat", + "host_ip": "11.22.33.44", + "host_name": "foo.com", + "id": "bar", + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1fQ.SSsX2Byyo1B1bGxV8C3G4QldhE5iH87EY_1r21-bwbI" + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".management-beats", + "type": "_doc", + "id": "tag:production", + "source": { + "type": "tag", + "tag": { + "configuration_blocks": [ + { + "type": "output", + "block_yml": "elasticsearch:\n hosts: [\"localhost:9200\"]\n username: \"...\"" + }, + { + "type": "metricbeat.modules", + "block_yml": "module: memcached\nhosts: [\"localhost:11211\"]" + } + ] + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".management-beats", + "type": "_doc", + "id": "tag:development", + "source": { + "type": "tag", + "tag": { + "configuration_blocks": [] + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".management-beats", + "type": "_doc", + "id": "tag:qa", + "source": { + "type": "tag", + "tag": { + "configuration_blocks": [ + { + "type": "metricbeat.modules", + "block_yml": "module: munin\nhosts: [\"localhost:4949\"]\nnode.namespace: node" + } + ] + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/beats/list/data.json.gz b/x-pack/test/functional/es_archives/beats/list/data.json.gz deleted file mode 100644 index 6af0e1b8aeb47b3ac9c5bea7dba0abc92e3fe9e4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 521 zcmV+k0`~nMiwFp){WeQOZ*BnXRb6k|Fcf{yuRwV&RveO!=53&s87Wi@ z#@bF(7IM;)v?a?%|>Z7)U=4GYBf}k6ZJ|0 zE9_?y*@!@dEc9qD2_V2Bp3#7YY15@RO?LEJ2j|d2R(TSUH0wFb4`{-(m{h%MwUW7K zHTF@(s_~}Gr*F6-H|I&}ut=sM&_N3r@3J8dUdlNKE{*}=L7nrWwi3DnF(EWboRlwV zDATm)&)ptj_pFb;l?VKRKBwcikmeIqc+rI&Vv>?G`?)4^1wBXEMe9rH?+IqmW z(!Lw6?UHNug6D&gQP^b%BerJv`<;d)Hnv4xd}A9X{SOE;}n8{Fu| zQ@td`vqW%zydnaNV(w)mWunVf9e>8^YxwiB>mxqj??KGfZhxFu9&)sfEROi2j@56N$h=!*J0l4bwm^~91m=dFGL LEy(|MkO}|*NeBrP diff --git a/x-pack/test/functional/es_archives/beats/list/mappings.json b/x-pack/test/functional/es_archives/beats/list/mappings.json index 0057b0b765773..caabcc13a9353 100644 --- a/x-pack/test/functional/es_archives/beats/list/mappings.json +++ b/x-pack/test/functional/es_archives/beats/list/mappings.json @@ -77,9 +77,6 @@ "tags": { "type": "keyword" }, - "central_configuration_yml": { - "type": "text" - }, "metadata": { "dynamic": "true", "type": "object" @@ -90,4 +87,4 @@ } } } -} +} \ No newline at end of file diff --git a/x-pack/yarn.lock b/x-pack/yarn.lock index 3fc10b7dd6909..0987c469e25a8 100644 --- a/x-pack/yarn.lock +++ b/x-pack/yarn.lock @@ -131,6 +131,10 @@ version "1.2.0" resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86" +"@types/expect.js@^0.3.29": + version "0.3.29" + resolved "https://registry.yarnpkg.com/@types/expect.js/-/expect.js-0.3.29.tgz#28dd359155b84b8ecb094afc3f4b74c3222dca3b" + "@types/form-data@^2.2.1": version "2.2.1" resolved "https://registry.yarnpkg.com/@types/form-data/-/form-data-2.2.1.tgz#ee2b3b8eaa11c0938289953606b745b738c54b1e" From afdca74ebe484d67afc9a94901a636297084000f Mon Sep 17 00:00:00 2001 From: Matt Apperson Date: Fri, 13 Jul 2018 12:16:37 -0400 Subject: [PATCH 23/43] fix bad rebase --- x-pack/plugins/beats/index.ts | 23 ++--- .../lib/adapters/beats/adapter_types.ts | 1 - .../beats/elasticsearch_beats_adapter.ts | 28 ------ .../adapters/beats/memory_beats_adapter.ts | 17 ---- .../kibana/kibana_framework_adapter.ts | 99 ------------------- .../kibana/testing_framework_adapter.ts | 79 --------------- .../framework/__tests__/kibana.test.ts | 2 +- .../domains/__tests__/beats/enroll.test.ts | 1 + x-pack/yarn.lock | 4 - 9 files changed, 14 insertions(+), 240 deletions(-) delete mode 100644 x-pack/plugins/beats/server/lib/adapters/famework/kibana/kibana_framework_adapter.ts delete mode 100644 x-pack/plugins/beats/server/lib/adapters/famework/kibana/testing_framework_adapter.ts diff --git a/x-pack/plugins/beats/index.ts b/x-pack/plugins/beats/index.ts index ced89c186f73e..c62d1a6805225 100644 --- a/x-pack/plugins/beats/index.ts +++ b/x-pack/plugins/beats/index.ts @@ -3,25 +3,26 @@ * 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 { PLUGIN } from './common/constants'; import { initServerWithKibana } from './server/kibana.index'; const DEFAULT_ENROLLMENT_TOKENS_TTL_S = 10 * 60; // 10 minutes +export const config = Joi.object({ + enabled: Joi.boolean().default(true), + encryptionKey: Joi.string(), + enrollmentTokensTtlInSeconds: Joi.number() + .integer() + .min(1) + .default(DEFAULT_ENROLLMENT_TOKENS_TTL_S), +}).default(); +export const configPrefix = 'xpack.beats'; + export function beats(kibana: any) { return new kibana.Plugin({ - config: () => - Joi.object({ - enabled: Joi.boolean().default(true), - encryptionKey: Joi.string(), - enrollmentTokensTtlInSeconds: Joi.number() - .integer() - .min(1) - .default(DEFAULT_ENROLLMENT_TOKENS_TTL_S), - }).default(), - configPrefix: 'xpack.beats', + config: () => config, + configPrefix, id: PLUGIN.ID, require: ['kibana', 'elasticsearch', 'xpack_main'], init(server: any) { diff --git a/x-pack/plugins/beats/server/lib/adapters/beats/adapter_types.ts b/x-pack/plugins/beats/server/lib/adapters/beats/adapter_types.ts index 8ce36456a703d..8812d9d39bd41 100644 --- a/x-pack/plugins/beats/server/lib/adapters/beats/adapter_types.ts +++ b/x-pack/plugins/beats/server/lib/adapters/beats/adapter_types.ts @@ -14,7 +14,6 @@ export interface CMBeatsAdapter { get(id: string): any; getAll(user: FrameworkUser): any; getWithIds(user: FrameworkUser, beatIds: string[]): any; - verifyBeats(user: FrameworkUser, beatIds: string[]): any; removeTagsFromBeats( user: FrameworkUser, removals: BeatsTagAssignment[] diff --git a/x-pack/plugins/beats/server/lib/adapters/beats/elasticsearch_beats_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/beats/elasticsearch_beats_adapter.ts index c0be74e1629f6..9a3d067ffa35f 100644 --- a/x-pack/plugins/beats/server/lib/adapters/beats/elasticsearch_beats_adapter.ts +++ b/x-pack/plugins/beats/server/lib/adapters/beats/elasticsearch_beats_adapter.ts @@ -5,7 +5,6 @@ */ import { flatten, get as _get, omit } from 'lodash'; -import moment from 'moment'; import { INDEX_NAMES } from '../../../../common/constants'; import { CMBeat } from '../../../../common/domain_types'; import { DatabaseAdapter } from '../database/adapter_types'; @@ -87,33 +86,6 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { .map((b: any) => b._source.beat); } - public async verifyBeats(user: FrameworkUser, beatIds: string[]) { - if (!Array.isArray(beatIds) || beatIds.length === 0) { - return []; - } - - const verifiedOn = moment().toJSON(); - const body = flatten( - beatIds.map(beatId => [ - { update: { _id: `beat:${beatId}` } }, - { doc: { beat: { verified_on: verifiedOn } } }, - ]) - ); - - const response = await this.database.bulk(user, { - _sourceInclude: ['beat.id', 'beat.verified_on'], - body, - index: INDEX_NAMES.BEATS, - refresh: 'wait_for', - type: '_doc', - }); - - return _get(response, 'items', []).map(b => ({ - ..._get(b, 'update.get._source.beat', {}), - updateStatus: _get(b, 'update.result', 'unknown error'), - })); - } - public async getAll(user: FrameworkUser) { const params = { index: INDEX_NAMES.BEATS, diff --git a/x-pack/plugins/beats/server/lib/adapters/beats/memory_beats_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/beats/memory_beats_adapter.ts index 041b34d29b49e..1762ff93fc185 100644 --- a/x-pack/plugins/beats/server/lib/adapters/beats/memory_beats_adapter.ts +++ b/x-pack/plugins/beats/server/lib/adapters/beats/memory_beats_adapter.ts @@ -5,7 +5,6 @@ */ import { omit } from 'lodash'; -import moment from 'moment'; import { CMBeat } from '../../../../common/domain_types'; import { FrameworkUser } from '../framework/adapter_types'; @@ -39,22 +38,6 @@ export class MemoryBeatsAdapter implements CMBeatsAdapter { return this.beatsDB.filter(beat => beatIds.includes(beat.id)); } - public async verifyBeats(user: FrameworkUser, beatIds: string[]) { - if (!Array.isArray(beatIds) || beatIds.length === 0) { - return []; - } - - const verifiedOn = moment().toJSON(); - - this.beatsDB.forEach((beat, i) => { - if (beatIds.includes(beat.id)) { - this.beatsDB[i].verified_on = verifiedOn; - } - }); - - return this.beatsDB.filter(beat => beatIds.includes(beat.id)); - } - public async getAll(user: FrameworkUser) { return this.beatsDB.map((beat: any) => omit(beat, ['access_token'])); } diff --git a/x-pack/plugins/beats/server/lib/adapters/famework/kibana/kibana_framework_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/famework/kibana/kibana_framework_adapter.ts deleted file mode 100644 index 71703da8632ec..0000000000000 --- a/x-pack/plugins/beats/server/lib/adapters/famework/kibana/kibana_framework_adapter.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* - * 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 { - BackendFrameworkAdapter, - FrameworkRequest, - FrameworkRouteOptions, - WrappableRequest, -} from '../../../lib'; - -import { IStrictReply, Request, Server } from 'hapi'; -import { internalFrameworkRequest, wrapRequest } from '../../../../utils/wrap_request'; - -export class KibanaBackendFrameworkAdapter implements BackendFrameworkAdapter { - public version: string; - private server: Server; - private cryptoHash: string | null; - - constructor(hapiServer: Server) { - this.server = hapiServer; - this.version = hapiServer.plugins.kibana.status.plugin.version; - this.cryptoHash = null; - - this.validateConfig(); - } - - public getSetting(settingPath: string) { - // TODO type check server properly - if (settingPath === 'xpack.beats.encryptionKey') { - // @ts-ignore - return this.server.config().get(settingPath) || this.cryptoHash; - } - // @ts-ignore - return this.server.config().get(settingPath) || this.cryptoHash; - } - - public exposeStaticDir(urlPath: string, dir: string): void { - this.server.route({ - handler: { - directory: { - path: dir, - }, - }, - method: 'GET', - path: urlPath, - }); - } - - public registerRoute( - route: FrameworkRouteOptions - ) { - const wrappedHandler = (request: any, reply: IStrictReply) => - route.handler(wrapRequest(request), reply); - - this.server.route({ - config: route.config, - handler: wrappedHandler, - method: route.method, - path: route.path, - }); - } - - public installIndexTemplate(name: string, template: {}) { - return this.callWithInternalUser('indices.putTemplate', { - body: template, - name, - }); - } - - public async callWithInternalUser(esMethod: string, options: {}) { - const { elasticsearch } = this.server.plugins; - const { callWithInternalUser } = elasticsearch.getCluster('admin'); - return await callWithInternalUser(esMethod, options); - } - - public async callWithRequest(req: FrameworkRequest, ...rest: any[]) { - const internalRequest = req[internalFrameworkRequest]; - const { elasticsearch } = internalRequest.server.plugins; - const { callWithRequest } = elasticsearch.getCluster('data'); - const fields = await callWithRequest(internalRequest, ...rest); - return fields; - } - - private validateConfig() { - // @ts-ignore - const config = this.server.config(); - const encryptionKey = config.get('xpack.beats.encryptionKey'); - - if (!encryptionKey) { - this.server.log( - 'Using a default encryption key for xpack.beats.encryptionKey. It is recommended that you set xpack.beats.encryptionKey in kibana.yml with a unique token' - ); - this.cryptoHash = 'xpack_beats_default_encryptionKey'; - } - } -} diff --git a/x-pack/plugins/beats/server/lib/adapters/famework/kibana/testing_framework_adapter.ts b/x-pack/plugins/beats/server/lib/adapters/famework/kibana/testing_framework_adapter.ts deleted file mode 100644 index 757464fa6cdc7..0000000000000 --- a/x-pack/plugins/beats/server/lib/adapters/famework/kibana/testing_framework_adapter.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* - * 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 { Client } from 'elasticsearch'; -import { Request } from 'hapi'; -import { get } from 'lodash'; -import { - BackendFrameworkAdapter, - FrameworkRequest, - FrameworkRouteOptions, - WrappableRequest, -} from '../../../lib'; - -interface TestSettings { - enrollmentTokensTtlInSeconds: number; - encryptionKey: string; -} - -export class TestingBackendFrameworkAdapter implements BackendFrameworkAdapter { - public version: string; - private client: Client | null; - private settings: TestSettings; - - constructor(client: Client | null, settings: TestSettings) { - this.client = client; - this.settings = settings || { - encryptionKey: 'something_who_cares', - enrollmentTokensTtlInSeconds: 10 * 60, // 10 minutes - }; - this.version = 'testing'; - } - - public getSetting(settingPath: string) { - switch (settingPath) { - case 'xpack.beats.enrollmentTokensTtlInSeconds': - return this.settings.enrollmentTokensTtlInSeconds; - case 'xpack.beats.encryptionKey': - return this.settings.encryptionKey; - } - } - - public exposeStaticDir(urlPath: string, dir: string): void { - // not yet testable - } - - public registerRoute( - route: FrameworkRouteOptions - ) { - // not yet testable - } - - public installIndexTemplate(name: string, template: {}) { - if (this.client) { - return this.client.indices.putTemplate({ - body: template, - name, - }); - } - } - - public async callWithInternalUser(esMethod: string, options: {}) { - const api = get(this.client, esMethod); - - api(options); - - return await api(options); - } - - public async callWithRequest(req: FrameworkRequest, esMethod: string, options: {}) { - const api = get(this.client, esMethod); - - api(options); - - return await api(options); - } -} diff --git a/x-pack/plugins/beats/server/lib/adapters/framework/__tests__/kibana.test.ts b/x-pack/plugins/beats/server/lib/adapters/framework/__tests__/kibana.test.ts index 5a539ebe6e5e7..74063e6316ceb 100644 --- a/x-pack/plugins/beats/server/lib/adapters/framework/__tests__/kibana.test.ts +++ b/x-pack/plugins/beats/server/lib/adapters/framework/__tests__/kibana.test.ts @@ -7,9 +7,9 @@ // @ts-ignore import { createEsTestCluster } from '@kbn/test'; -import { config as beatsPluginConfig, configPrefix } from '../../../../..'; // @ts-ignore import * as kbnTestServer from '../../../../../../../../src/test_utils/kbn_server'; +import { config as beatsPluginConfig, configPrefix } from '../../../../../index'; import { KibanaBackendFrameworkAdapter } from '../kibana_framework_adapter'; import { contractTests } from './test_contract'; diff --git a/x-pack/plugins/beats/server/lib/domains/__tests__/beats/enroll.test.ts b/x-pack/plugins/beats/server/lib/domains/__tests__/beats/enroll.test.ts index f60c1ed0e009e..9f42ad3c89f86 100644 --- a/x-pack/plugins/beats/server/lib/domains/__tests__/beats/enroll.test.ts +++ b/x-pack/plugins/beats/server/lib/domains/__tests__/beats/enroll.test.ts @@ -98,6 +98,7 @@ describe('Beats Domain Lib', () => { expect(beatsDB.length).toEqual(1); expect(beatsDB[0]).toHaveProperty('host_ip'); + expect(beatsDB[0]).toHaveProperty('verified_on'); expect(accessToken).toEqual(beatsDB[0].access_token); diff --git a/x-pack/yarn.lock b/x-pack/yarn.lock index 0987c469e25a8..3fc10b7dd6909 100644 --- a/x-pack/yarn.lock +++ b/x-pack/yarn.lock @@ -131,10 +131,6 @@ version "1.2.0" resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86" -"@types/expect.js@^0.3.29": - version "0.3.29" - resolved "https://registry.yarnpkg.com/@types/expect.js/-/expect.js-0.3.29.tgz#28dd359155b84b8ecb094afc3f4b74c3222dca3b" - "@types/form-data@^2.2.1": version "2.2.1" resolved "https://registry.yarnpkg.com/@types/form-data/-/form-data-2.2.1.tgz#ee2b3b8eaa11c0938289953606b745b738c54b1e" From 94be8ff98d1ac596ad807828b0bf5e6eeabd1d66 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Tue, 17 Jul 2018 14:58:32 -0400 Subject: [PATCH 24/43] [Beats Management] [WIP] Create public resources for management plugin (#20864) * Init plugin public resources. * rename beats to beats_management * rendering react now --- x-pack/index.js | 2 +- x-pack/package.json | 2 + .../common/constants/configuration_blocks.ts | 0 .../common/constants/index.ts | 1 + .../common/constants/index_names.ts | 0 .../common/constants/plugin.ts | 2 +- .../common/domain_types.ts | 0 .../{beats => beats_management}/index.ts | 10 +- .../plugins/beats_management/public/index.tsx | 22 +++ .../framework/kibana_framework_adapter.ts | 149 ++++++++++++++++++ .../public/lib/compose/kibana.ts | 29 ++++ .../beats_management/public/lib/lib.ts | 62 ++++++++ .../beats_management/public/pages/404.tsx | 13 ++ .../beats_management/public/pages/home.tsx | 13 ++ .../beats_management/public/routes.tsx | 22 +++ .../{beats => beats_management}/readme.md | 0 .../server/kibana.index.ts | 0 .../lib/adapters/beats/adapter_types.ts | 0 .../beats/elasticsearch_beats_adapter.ts | 0 .../adapters/beats/memory_beats_adapter.ts | 0 .../database/__tests__/kibana.test.ts | 0 .../database/__tests__/test_contract.ts | 0 .../lib/adapters/database/adapter_types.ts | 0 .../database/kibana_database_adapter.ts | 0 .../framework/__tests__/kibana.test.ts | 2 +- .../framework/__tests__/test_contract.ts | 0 .../lib/adapters/framework/adapter_types.ts | 0 .../framework/kibana_framework_adapter.ts | 0 .../framework/testing_framework_adapter.ts | 0 .../server/lib/adapters/tags/adapter_types.ts | 0 .../tags/elasticsearch_tags_adapter.ts | 2 +- .../lib/adapters/tags/memory_tags_adapter.ts | 0 .../lib/adapters/tokens/adapter_types.ts | 0 .../tokens/elasticsearch_tokens_adapter.ts | 0 .../adapters/tokens/memory_tokens_adapter.ts | 0 .../server/lib/compose/kibana.ts | 0 .../__tests__/beats/assign_tags.test.ts | 0 .../domains/__tests__/beats/enroll.test.ts | 0 .../lib/domains/__tests__/tokens.test.ts | 0 .../server/lib/domains/beats.ts | 2 +- .../server/lib/domains/tags.ts | 2 +- .../server/lib/domains/tokens.ts | 2 +- .../server/lib/lib.ts | 0 .../server/management_server.ts | 0 .../server/rest_api/beats/configuration.ts | 0 .../server/rest_api/beats/enroll.ts | 2 +- .../server/rest_api/beats/list.ts | 0 .../server/rest_api/beats/tag_assignment.ts | 0 .../server/rest_api/beats/tag_removal.ts | 0 .../server/rest_api/beats/update.ts | 0 .../server/rest_api/tags/set.ts | 0 .../server/rest_api/tokens/create.ts | 0 .../server/utils/README.md | 0 .../server/utils/error_wrappers/index.ts | 0 .../error_wrappers/wrap_es_error.test.ts | 0 .../utils/error_wrappers/wrap_es_error.ts | 0 .../server/utils/find_non_existent_items.ts | 0 .../utils/index_templates/beats_template.json | 0 .../server/utils/index_templates/index.ts | 0 .../server/utils/polyfills.ts | 0 .../server/utils/wrap_request.ts | 0 .../{beats => beats_management}/tsconfig.json | 0 .../types/json.t.ts | 0 .../{beats => beats_management}/wallaby.js | 0 x-pack/plugins/index_management/index.js | 8 +- x-pack/yarn.lock | 29 ++++ 66 files changed, 361 insertions(+), 15 deletions(-) rename x-pack/plugins/{beats => beats_management}/common/constants/configuration_blocks.ts (100%) rename x-pack/plugins/{beats => beats_management}/common/constants/index.ts (87%) rename x-pack/plugins/{beats => beats_management}/common/constants/index_names.ts (100%) rename x-pack/plugins/{beats => beats_management}/common/constants/plugin.ts (91%) rename x-pack/plugins/{beats => beats_management}/common/domain_types.ts (100%) rename x-pack/plugins/{beats => beats_management}/index.ts (86%) create mode 100644 x-pack/plugins/beats_management/public/index.tsx create mode 100644 x-pack/plugins/beats_management/public/lib/adapters/framework/kibana_framework_adapter.ts create mode 100644 x-pack/plugins/beats_management/public/lib/compose/kibana.ts create mode 100644 x-pack/plugins/beats_management/public/lib/lib.ts create mode 100644 x-pack/plugins/beats_management/public/pages/404.tsx create mode 100644 x-pack/plugins/beats_management/public/pages/home.tsx create mode 100644 x-pack/plugins/beats_management/public/routes.tsx rename x-pack/plugins/{beats => beats_management}/readme.md (100%) rename x-pack/plugins/{beats => beats_management}/server/kibana.index.ts (100%) rename x-pack/plugins/{beats => beats_management}/server/lib/adapters/beats/adapter_types.ts (100%) rename x-pack/plugins/{beats => beats_management}/server/lib/adapters/beats/elasticsearch_beats_adapter.ts (100%) rename x-pack/plugins/{beats => beats_management}/server/lib/adapters/beats/memory_beats_adapter.ts (100%) rename x-pack/plugins/{beats => beats_management}/server/lib/adapters/database/__tests__/kibana.test.ts (100%) rename x-pack/plugins/{beats => beats_management}/server/lib/adapters/database/__tests__/test_contract.ts (100%) rename x-pack/plugins/{beats => beats_management}/server/lib/adapters/database/adapter_types.ts (100%) rename x-pack/plugins/{beats => beats_management}/server/lib/adapters/database/kibana_database_adapter.ts (100%) rename x-pack/plugins/{beats => beats_management}/server/lib/adapters/framework/__tests__/kibana.test.ts (98%) rename x-pack/plugins/{beats => beats_management}/server/lib/adapters/framework/__tests__/test_contract.ts (100%) rename x-pack/plugins/{beats => beats_management}/server/lib/adapters/framework/adapter_types.ts (100%) rename x-pack/plugins/{beats => beats_management}/server/lib/adapters/framework/kibana_framework_adapter.ts (100%) rename x-pack/plugins/{beats => beats_management}/server/lib/adapters/framework/testing_framework_adapter.ts (100%) rename x-pack/plugins/{beats => beats_management}/server/lib/adapters/tags/adapter_types.ts (100%) rename x-pack/plugins/{beats => beats_management}/server/lib/adapters/tags/elasticsearch_tags_adapter.ts (96%) rename x-pack/plugins/{beats => beats_management}/server/lib/adapters/tags/memory_tags_adapter.ts (100%) rename x-pack/plugins/{beats => beats_management}/server/lib/adapters/tokens/adapter_types.ts (100%) rename x-pack/plugins/{beats => beats_management}/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts (100%) rename x-pack/plugins/{beats => beats_management}/server/lib/adapters/tokens/memory_tokens_adapter.ts (100%) rename x-pack/plugins/{beats => beats_management}/server/lib/compose/kibana.ts (100%) rename x-pack/plugins/{beats => beats_management}/server/lib/domains/__tests__/beats/assign_tags.test.ts (100%) rename x-pack/plugins/{beats => beats_management}/server/lib/domains/__tests__/beats/enroll.test.ts (100%) rename x-pack/plugins/{beats => beats_management}/server/lib/domains/__tests__/tokens.test.ts (100%) rename x-pack/plugins/{beats => beats_management}/server/lib/domains/beats.ts (98%) rename x-pack/plugins/{beats => beats_management}/server/lib/domains/tags.ts (98%) rename x-pack/plugins/{beats => beats_management}/server/lib/domains/tokens.ts (98%) rename x-pack/plugins/{beats => beats_management}/server/lib/lib.ts (100%) rename x-pack/plugins/{beats => beats_management}/server/management_server.ts (100%) rename x-pack/plugins/{beats => beats_management}/server/rest_api/beats/configuration.ts (100%) rename x-pack/plugins/{beats => beats_management}/server/rest_api/beats/enroll.ts (97%) rename x-pack/plugins/{beats => beats_management}/server/rest_api/beats/list.ts (100%) rename x-pack/plugins/{beats => beats_management}/server/rest_api/beats/tag_assignment.ts (100%) rename x-pack/plugins/{beats => beats_management}/server/rest_api/beats/tag_removal.ts (100%) rename x-pack/plugins/{beats => beats_management}/server/rest_api/beats/update.ts (100%) rename x-pack/plugins/{beats => beats_management}/server/rest_api/tags/set.ts (100%) rename x-pack/plugins/{beats => beats_management}/server/rest_api/tokens/create.ts (100%) rename x-pack/plugins/{beats => beats_management}/server/utils/README.md (100%) rename x-pack/plugins/{beats => beats_management}/server/utils/error_wrappers/index.ts (100%) rename x-pack/plugins/{beats => beats_management}/server/utils/error_wrappers/wrap_es_error.test.ts (100%) rename x-pack/plugins/{beats => beats_management}/server/utils/error_wrappers/wrap_es_error.ts (100%) rename x-pack/plugins/{beats => beats_management}/server/utils/find_non_existent_items.ts (100%) rename x-pack/plugins/{beats => beats_management}/server/utils/index_templates/beats_template.json (100%) rename x-pack/plugins/{beats => beats_management}/server/utils/index_templates/index.ts (100%) rename x-pack/plugins/{beats => beats_management}/server/utils/polyfills.ts (100%) rename x-pack/plugins/{beats => beats_management}/server/utils/wrap_request.ts (100%) rename x-pack/plugins/{beats => beats_management}/tsconfig.json (100%) rename x-pack/plugins/{beats => beats_management}/types/json.t.ts (100%) rename x-pack/plugins/{beats => beats_management}/wallaby.js (100%) diff --git a/x-pack/index.js b/x-pack/index.js index 6f5c12814997a..2a91738c67a32 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -16,7 +16,7 @@ import { watcher } from './plugins/watcher'; import { grokdebugger } from './plugins/grokdebugger'; import { dashboardMode } from './plugins/dashboard_mode'; import { logstash } from './plugins/logstash'; -import { beats } from './plugins/beats'; +import { beats } from './plugins/beats_management'; import { apm } from './plugins/apm'; import { licenseManagement } from './plugins/license_management'; import { cloud } from './plugins/cloud'; diff --git a/x-pack/package.json b/x-pack/package.json index 9cf203d0c6b83..fc7d7079174ad 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -29,10 +29,12 @@ "@kbn/test": "link:../packages/kbn-test", "@types/boom": "^4.3.8", "@types/chance": "^1.0.1", + "@types/history": "^4.6.2", "@types/jest": "^22.2.3", "@types/joi": "^10.4.0", "@types/lodash": "^3.10.0", "@types/pngjs": "^3.3.0", + "@types/react-router-dom": "^4.2.7", "@types/sinon": "^5.0.1", "abab": "^1.0.4", "ansicolors": "0.3.2", diff --git a/x-pack/plugins/beats/common/constants/configuration_blocks.ts b/x-pack/plugins/beats_management/common/constants/configuration_blocks.ts similarity index 100% rename from x-pack/plugins/beats/common/constants/configuration_blocks.ts rename to x-pack/plugins/beats_management/common/constants/configuration_blocks.ts diff --git a/x-pack/plugins/beats/common/constants/index.ts b/x-pack/plugins/beats_management/common/constants/index.ts similarity index 87% rename from x-pack/plugins/beats/common/constants/index.ts rename to x-pack/plugins/beats_management/common/constants/index.ts index 756ffcf07e3ea..b4e919607c604 100644 --- a/x-pack/plugins/beats/common/constants/index.ts +++ b/x-pack/plugins/beats_management/common/constants/index.ts @@ -7,3 +7,4 @@ export { PLUGIN } from './plugin'; export { INDEX_NAMES } from './index_names'; export { UNIQUENESS_ENFORCING_TYPES, ConfigurationBlockTypes } from './configuration_blocks'; +export const BASE_PATH = '/management/beats_management/'; diff --git a/x-pack/plugins/beats/common/constants/index_names.ts b/x-pack/plugins/beats_management/common/constants/index_names.ts similarity index 100% rename from x-pack/plugins/beats/common/constants/index_names.ts rename to x-pack/plugins/beats_management/common/constants/index_names.ts diff --git a/x-pack/plugins/beats/common/constants/plugin.ts b/x-pack/plugins/beats_management/common/constants/plugin.ts similarity index 91% rename from x-pack/plugins/beats/common/constants/plugin.ts rename to x-pack/plugins/beats_management/common/constants/plugin.ts index ba12300075bf2..dc7cd85300341 100644 --- a/x-pack/plugins/beats/common/constants/plugin.ts +++ b/x-pack/plugins/beats_management/common/constants/plugin.ts @@ -5,5 +5,5 @@ */ export const PLUGIN = { - ID: 'beats', + ID: 'beats_management', }; diff --git a/x-pack/plugins/beats/common/domain_types.ts b/x-pack/plugins/beats_management/common/domain_types.ts similarity index 100% rename from x-pack/plugins/beats/common/domain_types.ts rename to x-pack/plugins/beats_management/common/domain_types.ts diff --git a/x-pack/plugins/beats/index.ts b/x-pack/plugins/beats_management/index.ts similarity index 86% rename from x-pack/plugins/beats/index.ts rename to x-pack/plugins/beats_management/index.ts index c62d1a6805225..e6677d6bbcd57 100644 --- a/x-pack/plugins/beats/index.ts +++ b/x-pack/plugins/beats_management/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import Joi from 'joi'; +import { resolve } from 'path'; import { PLUGIN } from './common/constants'; import { initServerWithKibana } from './server/kibana.index'; @@ -21,10 +22,15 @@ export const configPrefix = 'xpack.beats'; export function beats(kibana: any) { return new kibana.Plugin({ - config: () => config, - configPrefix, id: PLUGIN.ID, require: ['kibana', 'elasticsearch', 'xpack_main'], + publicDir: resolve(__dirname, 'public'), + uiExports: { + managementSections: ['plugins/beats_management'], + }, + config: () => config, + configPrefix, + init(server: any) { initServerWithKibana(server); }, diff --git a/x-pack/plugins/beats_management/public/index.tsx b/x-pack/plugins/beats_management/public/index.tsx new file mode 100644 index 0000000000000..1334deb1524e6 --- /dev/null +++ b/x-pack/plugins/beats_management/public/index.tsx @@ -0,0 +1,22 @@ +/* + * 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 React from 'react'; +import { BASE_PATH } from '../common/constants'; +import { compose } from './lib/compose/kibana'; +// import * as euiVars from '@elastic/eui/dist/eui_theme_k6_light.json'; +// import { ThemeProvider } from 'styled-components'; +import { PageRouter } from './routes'; + +// TODO use theme provided from parentApp when kibana supports it +import '@elastic/eui/dist/eui_theme_light.css'; + +function startApp(libs: any) { + libs.framework.registerManagementSection('beats', 'Beats Management', BASE_PATH); + libs.framework.render(); +} + +startApp(compose()); diff --git a/x-pack/plugins/beats_management/public/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/framework/kibana_framework_adapter.ts new file mode 100644 index 0000000000000..042ebd71f9862 --- /dev/null +++ b/x-pack/plugins/beats_management/public/lib/adapters/framework/kibana_framework_adapter.ts @@ -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 { IModule, IScope } from 'angular'; +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; + +import { + BufferedKibanaServiceCall, + FrameworkAdapter, + KibanaAdapterServiceRefs, + KibanaUIConfig, +} from '../../lib'; + +export class KibanaFrameworkAdapter implements FrameworkAdapter { + public appState: object; + public kbnVersion?: string; + + private management: any; + private adapterService: KibanaAdapterServiceProvider; + private rootComponent: React.ReactElement | null = null; + private uiModule: IModule; + private routes: any; + + constructor(uiModule: IModule, management: any, routes: any) { + this.adapterService = new KibanaAdapterServiceProvider(); + this.management = management; + this.uiModule = uiModule; + this.routes = routes; + this.appState = {}; + } + + public setUISettings = (key: string, value: any) => { + this.adapterService.callOrBuffer(({ config }) => { + config.set(key, value); + }); + }; + + public render = (component: React.ReactElement) => { + this.rootComponent = component; + }; + + public registerManagementSection(pluginId: string, displayName: string, basePath: string) { + const registerSection = () => + this.management.register(pluginId, { + display: displayName, + order: 30, + }); + const getSection = () => this.management.getSection(pluginId); + + const section = this.management.hasItem(pluginId) ? getSection() : registerSection(); + + section.register(pluginId, { + visible: true, + display: displayName, + order: 30, + url: `#${basePath}`, + }); + + this.register(this.uiModule); + } + + private manageAngularLifecycle($scope: any, $route: any, elem: any) { + const lastRoute = $route.current; + const deregister = $scope.$on('$locationChangeSuccess', () => { + const currentRoute = $route.current; + // if templates are the same we are on the same route + if (lastRoute.$$route.template === currentRoute.$$route.template) { + // this prevents angular from destroying scope + $route.current = lastRoute; + } + }); + $scope.$on('$destroy', () => { + if (deregister) { + deregister(); + } + // manually unmount component when scope is destroyed + if (elem) { + ReactDOM.unmountComponentAtNode(elem); + } + }); + } + + private register = (adapterModule: IModule) => { + const adapter = this; + this.routes.when(`/management/beats_management/?`, { + template: '
', + controllerAs: 'beatsManagement', + // tslint:disable-next-line: max-classes-per-file + controller: class BeatsManagementController { + constructor($scope: any, $route: any) { + $scope.$$postDigest(() => { + const elem = document.getElementById('beatsReactRoot'); + ReactDOM.render(adapter.rootComponent as React.ReactElement, elem); + adapter.manageAngularLifecycle($scope, $route, elem); + }); + $scope.$onInit = () => { + $scope.topNavMenu = []; + }; + } + }, + }); + }; +} + +// tslint:disable-next-line: max-classes-per-file +class KibanaAdapterServiceProvider { + public serviceRefs: KibanaAdapterServiceRefs | null = null; + public bufferedCalls: Array> = []; + + public $get($rootScope: IScope, config: KibanaUIConfig) { + this.serviceRefs = { + config, + rootScope: $rootScope, + }; + + this.applyBufferedCalls(this.bufferedCalls); + + return this; + } + + public callOrBuffer(serviceCall: (serviceRefs: KibanaAdapterServiceRefs) => void) { + if (this.serviceRefs !== null) { + this.applyBufferedCalls([serviceCall]); + } else { + this.bufferedCalls.push(serviceCall); + } + } + + public applyBufferedCalls( + bufferedCalls: Array> + ) { + if (!this.serviceRefs) { + return; + } + + this.serviceRefs.rootScope.$apply(() => { + bufferedCalls.forEach(serviceCall => { + if (!this.serviceRefs) { + return; + } + return serviceCall(this.serviceRefs); + }); + }); + } +} diff --git a/x-pack/plugins/beats_management/public/lib/compose/kibana.ts b/x-pack/plugins/beats_management/public/lib/compose/kibana.ts new file mode 100644 index 0000000000000..7a98e62c18459 --- /dev/null +++ b/x-pack/plugins/beats_management/public/lib/compose/kibana.ts @@ -0,0 +1,29 @@ +/* + * 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 'ui/autoload/all'; +// @ts-ignore: path dynamic for kibana +import { management } from 'ui/management'; +// @ts-ignore: path dynamic for kibana +import { uiModules } from 'ui/modules'; +// @ts-ignore: path dynamic for kibana +import routes from 'ui/routes'; +// @ts-ignore: path dynamic for kibana +import { KibanaFrameworkAdapter } from '../adapters/framework/kibana_framework_adapter'; +import { FrontendLibs } from '../lib'; + +export function compose(): FrontendLibs { + // const kbnVersion = (window as any).__KBN__.version; + + const pluginUIModule = uiModules.get('app/beats_management'); + + const framework = new KibanaFrameworkAdapter(pluginUIModule, management, routes); + + const libs: FrontendLibs = { + framework, + }; + return libs; +} diff --git a/x-pack/plugins/beats_management/public/lib/lib.ts b/x-pack/plugins/beats_management/public/lib/lib.ts new file mode 100644 index 0000000000000..085efc3e53201 --- /dev/null +++ b/x-pack/plugins/beats_management/public/lib/lib.ts @@ -0,0 +1,62 @@ +/* + * 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 { IModule, IScope } from 'angular'; +import { AxiosRequestConfig } from 'axios'; +import React from 'react'; + +export interface FrontendLibs { + framework: FrameworkAdapter; + // api: ApiAdapter; +} + +export interface FrameworkAdapter { + // Insstance vars + appState?: object; + kbnVersion?: string; + registerManagementSection(pluginId: string, displayName: string, basePath: string): void; + + // Methods + setUISettings(key: string, value: any): void; + render(component: React.ReactElement): void; +} + +export interface FramworkAdapterConstructable { + new (uiModule: IModule): FrameworkAdapter; +} + +// TODO: replace AxiosRequestConfig with something more defined +export type RequestConfig = AxiosRequestConfig; + +export interface ApiAdapter { + kbnVersion: string; + + get(url: string, config?: RequestConfig | undefined): Promise; + post(url: string, data?: any, config?: AxiosRequestConfig | undefined): Promise; + delete(url: string, config?: RequestConfig | undefined): Promise; + put(url: string, data?: any, config?: RequestConfig | undefined): Promise; +} + +export interface UiKibanaAdapterScope extends IScope { + breadcrumbs: any[]; + topNavMenu: any[]; +} + +export interface KibanaUIConfig { + get(key: string): any; + set(key: string, value: any): Promise; +} + +export interface KibanaAdapterServiceRefs { + config: KibanaUIConfig; + rootScope: IScope; +} + +export type BufferedKibanaServiceCall = (serviceRefs: ServiceRefs) => void; + +export interface Chrome { + setRootTemplate(template: string): void; +} diff --git a/x-pack/plugins/beats_management/public/pages/404.tsx b/x-pack/plugins/beats_management/public/pages/404.tsx new file mode 100644 index 0000000000000..956bf90e84927 --- /dev/null +++ b/x-pack/plugins/beats_management/public/pages/404.tsx @@ -0,0 +1,13 @@ +/* + * 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 React from 'react'; + +export class NotFoundPage extends React.PureComponent { + public render() { + return
No content found
; + } +} diff --git a/x-pack/plugins/beats_management/public/pages/home.tsx b/x-pack/plugins/beats_management/public/pages/home.tsx new file mode 100644 index 0000000000000..505015f18af16 --- /dev/null +++ b/x-pack/plugins/beats_management/public/pages/home.tsx @@ -0,0 +1,13 @@ +/* + * 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 React from 'react'; + +export class HomePage extends React.PureComponent { + public render() { + return
Home
; + } +} diff --git a/x-pack/plugins/beats_management/public/routes.tsx b/x-pack/plugins/beats_management/public/routes.tsx new file mode 100644 index 0000000000000..f5863b28aaafa --- /dev/null +++ b/x-pack/plugins/beats_management/public/routes.tsx @@ -0,0 +1,22 @@ +/* + * 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 React from 'react'; +import { HashRouter, Route, Switch } from 'react-router-dom'; + +import { NotFoundPage } from './pages/404'; +import { HomePage } from './pages/home'; + +export const PageRouter: React.SFC<{}> = () => { + return ( + + + + + + + ); +}; diff --git a/x-pack/plugins/beats/readme.md b/x-pack/plugins/beats_management/readme.md similarity index 100% rename from x-pack/plugins/beats/readme.md rename to x-pack/plugins/beats_management/readme.md diff --git a/x-pack/plugins/beats/server/kibana.index.ts b/x-pack/plugins/beats_management/server/kibana.index.ts similarity index 100% rename from x-pack/plugins/beats/server/kibana.index.ts rename to x-pack/plugins/beats_management/server/kibana.index.ts diff --git a/x-pack/plugins/beats/server/lib/adapters/beats/adapter_types.ts b/x-pack/plugins/beats_management/server/lib/adapters/beats/adapter_types.ts similarity index 100% rename from x-pack/plugins/beats/server/lib/adapters/beats/adapter_types.ts rename to x-pack/plugins/beats_management/server/lib/adapters/beats/adapter_types.ts diff --git a/x-pack/plugins/beats/server/lib/adapters/beats/elasticsearch_beats_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts similarity index 100% rename from x-pack/plugins/beats/server/lib/adapters/beats/elasticsearch_beats_adapter.ts rename to x-pack/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts diff --git a/x-pack/plugins/beats/server/lib/adapters/beats/memory_beats_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/beats/memory_beats_adapter.ts similarity index 100% rename from x-pack/plugins/beats/server/lib/adapters/beats/memory_beats_adapter.ts rename to x-pack/plugins/beats_management/server/lib/adapters/beats/memory_beats_adapter.ts diff --git a/x-pack/plugins/beats/server/lib/adapters/database/__tests__/kibana.test.ts b/x-pack/plugins/beats_management/server/lib/adapters/database/__tests__/kibana.test.ts similarity index 100% rename from x-pack/plugins/beats/server/lib/adapters/database/__tests__/kibana.test.ts rename to x-pack/plugins/beats_management/server/lib/adapters/database/__tests__/kibana.test.ts diff --git a/x-pack/plugins/beats/server/lib/adapters/database/__tests__/test_contract.ts b/x-pack/plugins/beats_management/server/lib/adapters/database/__tests__/test_contract.ts similarity index 100% rename from x-pack/plugins/beats/server/lib/adapters/database/__tests__/test_contract.ts rename to x-pack/plugins/beats_management/server/lib/adapters/database/__tests__/test_contract.ts diff --git a/x-pack/plugins/beats/server/lib/adapters/database/adapter_types.ts b/x-pack/plugins/beats_management/server/lib/adapters/database/adapter_types.ts similarity index 100% rename from x-pack/plugins/beats/server/lib/adapters/database/adapter_types.ts rename to x-pack/plugins/beats_management/server/lib/adapters/database/adapter_types.ts diff --git a/x-pack/plugins/beats/server/lib/adapters/database/kibana_database_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/database/kibana_database_adapter.ts similarity index 100% rename from x-pack/plugins/beats/server/lib/adapters/database/kibana_database_adapter.ts rename to x-pack/plugins/beats_management/server/lib/adapters/database/kibana_database_adapter.ts diff --git a/x-pack/plugins/beats/server/lib/adapters/framework/__tests__/kibana.test.ts b/x-pack/plugins/beats_management/server/lib/adapters/framework/__tests__/kibana.test.ts similarity index 98% rename from x-pack/plugins/beats/server/lib/adapters/framework/__tests__/kibana.test.ts rename to x-pack/plugins/beats_management/server/lib/adapters/framework/__tests__/kibana.test.ts index 74063e6316ceb..5a539ebe6e5e7 100644 --- a/x-pack/plugins/beats/server/lib/adapters/framework/__tests__/kibana.test.ts +++ b/x-pack/plugins/beats_management/server/lib/adapters/framework/__tests__/kibana.test.ts @@ -7,9 +7,9 @@ // @ts-ignore import { createEsTestCluster } from '@kbn/test'; +import { config as beatsPluginConfig, configPrefix } from '../../../../..'; // @ts-ignore import * as kbnTestServer from '../../../../../../../../src/test_utils/kbn_server'; -import { config as beatsPluginConfig, configPrefix } from '../../../../../index'; import { KibanaBackendFrameworkAdapter } from '../kibana_framework_adapter'; import { contractTests } from './test_contract'; diff --git a/x-pack/plugins/beats/server/lib/adapters/framework/__tests__/test_contract.ts b/x-pack/plugins/beats_management/server/lib/adapters/framework/__tests__/test_contract.ts similarity index 100% rename from x-pack/plugins/beats/server/lib/adapters/framework/__tests__/test_contract.ts rename to x-pack/plugins/beats_management/server/lib/adapters/framework/__tests__/test_contract.ts diff --git a/x-pack/plugins/beats/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts similarity index 100% rename from x-pack/plugins/beats/server/lib/adapters/framework/adapter_types.ts rename to x-pack/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts diff --git a/x-pack/plugins/beats/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts similarity index 100% rename from x-pack/plugins/beats/server/lib/adapters/framework/kibana_framework_adapter.ts rename to x-pack/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts diff --git a/x-pack/plugins/beats/server/lib/adapters/framework/testing_framework_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/framework/testing_framework_adapter.ts similarity index 100% rename from x-pack/plugins/beats/server/lib/adapters/framework/testing_framework_adapter.ts rename to x-pack/plugins/beats_management/server/lib/adapters/framework/testing_framework_adapter.ts diff --git a/x-pack/plugins/beats/server/lib/adapters/tags/adapter_types.ts b/x-pack/plugins/beats_management/server/lib/adapters/tags/adapter_types.ts similarity index 100% rename from x-pack/plugins/beats/server/lib/adapters/tags/adapter_types.ts rename to x-pack/plugins/beats_management/server/lib/adapters/tags/adapter_types.ts diff --git a/x-pack/plugins/beats/server/lib/adapters/tags/elasticsearch_tags_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/tags/elasticsearch_tags_adapter.ts similarity index 96% rename from x-pack/plugins/beats/server/lib/adapters/tags/elasticsearch_tags_adapter.ts rename to x-pack/plugins/beats_management/server/lib/adapters/tags/elasticsearch_tags_adapter.ts index 3f982f5edbb09..2c2c988189283 100644 --- a/x-pack/plugins/beats/server/lib/adapters/tags/elasticsearch_tags_adapter.ts +++ b/x-pack/plugins/beats_management/server/lib/adapters/tags/elasticsearch_tags_adapter.ts @@ -6,7 +6,7 @@ import { get } from 'lodash'; import { INDEX_NAMES } from '../../../../common/constants'; -import { FrameworkUser } from './../framework/adapter_types'; +import { FrameworkUser } from '../framework/adapter_types'; import { BeatTag } from '../../../../common/domain_types'; import { DatabaseAdapter } from '../database/adapter_types'; diff --git a/x-pack/plugins/beats/server/lib/adapters/tags/memory_tags_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/tags/memory_tags_adapter.ts similarity index 100% rename from x-pack/plugins/beats/server/lib/adapters/tags/memory_tags_adapter.ts rename to x-pack/plugins/beats_management/server/lib/adapters/tags/memory_tags_adapter.ts diff --git a/x-pack/plugins/beats/server/lib/adapters/tokens/adapter_types.ts b/x-pack/plugins/beats_management/server/lib/adapters/tokens/adapter_types.ts similarity index 100% rename from x-pack/plugins/beats/server/lib/adapters/tokens/adapter_types.ts rename to x-pack/plugins/beats_management/server/lib/adapters/tokens/adapter_types.ts diff --git a/x-pack/plugins/beats/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts similarity index 100% rename from x-pack/plugins/beats/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts rename to x-pack/plugins/beats_management/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts diff --git a/x-pack/plugins/beats/server/lib/adapters/tokens/memory_tokens_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/tokens/memory_tokens_adapter.ts similarity index 100% rename from x-pack/plugins/beats/server/lib/adapters/tokens/memory_tokens_adapter.ts rename to x-pack/plugins/beats_management/server/lib/adapters/tokens/memory_tokens_adapter.ts diff --git a/x-pack/plugins/beats/server/lib/compose/kibana.ts b/x-pack/plugins/beats_management/server/lib/compose/kibana.ts similarity index 100% rename from x-pack/plugins/beats/server/lib/compose/kibana.ts rename to x-pack/plugins/beats_management/server/lib/compose/kibana.ts diff --git a/x-pack/plugins/beats/server/lib/domains/__tests__/beats/assign_tags.test.ts b/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/assign_tags.test.ts similarity index 100% rename from x-pack/plugins/beats/server/lib/domains/__tests__/beats/assign_tags.test.ts rename to x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/assign_tags.test.ts diff --git a/x-pack/plugins/beats/server/lib/domains/__tests__/beats/enroll.test.ts b/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/enroll.test.ts similarity index 100% rename from x-pack/plugins/beats/server/lib/domains/__tests__/beats/enroll.test.ts rename to x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/enroll.test.ts diff --git a/x-pack/plugins/beats/server/lib/domains/__tests__/tokens.test.ts b/x-pack/plugins/beats_management/server/lib/domains/__tests__/tokens.test.ts similarity index 100% rename from x-pack/plugins/beats/server/lib/domains/__tests__/tokens.test.ts rename to x-pack/plugins/beats_management/server/lib/domains/__tests__/tokens.test.ts diff --git a/x-pack/plugins/beats/server/lib/domains/beats.ts b/x-pack/plugins/beats_management/server/lib/domains/beats.ts similarity index 98% rename from x-pack/plugins/beats/server/lib/domains/beats.ts rename to x-pack/plugins/beats_management/server/lib/domains/beats.ts index 07a084e54b53d..618de8ab0b446 100644 --- a/x-pack/plugins/beats/server/lib/domains/beats.ts +++ b/x-pack/plugins/beats_management/server/lib/domains/beats.ts @@ -13,8 +13,8 @@ import { BeatsTagAssignment, CMBeatsAdapter } from '../adapters/beats/adapter_ty import { FrameworkUser } from '../adapters/framework/adapter_types'; import { CMAssignmentReturn } from '../adapters/beats/adapter_types'; +import { BeatsRemovalReturn } from '../adapters/beats/adapter_types'; import { BeatEnrollmentStatus, CMDomainLibs } from '../lib'; -import { BeatsRemovalReturn } from './../adapters/beats/adapter_types'; export class CMBeatsDomain { private adapter: CMBeatsAdapter; diff --git a/x-pack/plugins/beats/server/lib/domains/tags.ts b/x-pack/plugins/beats_management/server/lib/domains/tags.ts similarity index 98% rename from x-pack/plugins/beats/server/lib/domains/tags.ts rename to x-pack/plugins/beats_management/server/lib/domains/tags.ts index b6ce9f7e14821..36cf9a5e6d7f0 100644 --- a/x-pack/plugins/beats/server/lib/domains/tags.ts +++ b/x-pack/plugins/beats_management/server/lib/domains/tags.ts @@ -9,8 +9,8 @@ import { UNIQUENESS_ENFORCING_TYPES } from '../../../common/constants'; import { ConfigurationBlock } from '../../../common/domain_types'; import { FrameworkUser } from '../adapters/framework/adapter_types'; +import { entries } from '../../utils/polyfills'; import { CMTagsAdapter } from '../adapters/tags/adapter_types'; -import { entries } from './../../utils/polyfills'; export class CMTagsDomain { private adapter: CMTagsAdapter; diff --git a/x-pack/plugins/beats/server/lib/domains/tokens.ts b/x-pack/plugins/beats_management/server/lib/domains/tokens.ts similarity index 98% rename from x-pack/plugins/beats/server/lib/domains/tokens.ts rename to x-pack/plugins/beats_management/server/lib/domains/tokens.ts index dc7ffa9a63356..529a526bea75d 100644 --- a/x-pack/plugins/beats/server/lib/domains/tokens.ts +++ b/x-pack/plugins/beats_management/server/lib/domains/tokens.ts @@ -8,8 +8,8 @@ import { sign as signToken, verify as verifyToken } from 'jsonwebtoken'; import moment from 'moment'; import uuid from 'uuid'; import { BackendFrameworkAdapter } from '../adapters/framework/adapter_types'; +import { FrameworkUser } from '../adapters/framework/adapter_types'; import { CMTokensAdapter } from '../adapters/tokens/adapter_types'; -import { FrameworkUser } from './../adapters/framework/adapter_types'; const RANDOM_TOKEN_1 = 'b48c4bda384a40cb91c6eb9b8849e77f'; const RANDOM_TOKEN_2 = '80a3819e3cd64f4399f1d4886be7a08b'; diff --git a/x-pack/plugins/beats/server/lib/lib.ts b/x-pack/plugins/beats_management/server/lib/lib.ts similarity index 100% rename from x-pack/plugins/beats/server/lib/lib.ts rename to x-pack/plugins/beats_management/server/lib/lib.ts diff --git a/x-pack/plugins/beats/server/management_server.ts b/x-pack/plugins/beats_management/server/management_server.ts similarity index 100% rename from x-pack/plugins/beats/server/management_server.ts rename to x-pack/plugins/beats_management/server/management_server.ts diff --git a/x-pack/plugins/beats/server/rest_api/beats/configuration.ts b/x-pack/plugins/beats_management/server/rest_api/beats/configuration.ts similarity index 100% rename from x-pack/plugins/beats/server/rest_api/beats/configuration.ts rename to x-pack/plugins/beats_management/server/rest_api/beats/configuration.ts diff --git a/x-pack/plugins/beats/server/rest_api/beats/enroll.ts b/x-pack/plugins/beats_management/server/rest_api/beats/enroll.ts similarity index 97% rename from x-pack/plugins/beats/server/rest_api/beats/enroll.ts rename to x-pack/plugins/beats_management/server/rest_api/beats/enroll.ts index c5b01fdbd4cc3..c0f0185fbde25 100644 --- a/x-pack/plugins/beats/server/rest_api/beats/enroll.ts +++ b/x-pack/plugins/beats_management/server/rest_api/beats/enroll.ts @@ -7,8 +7,8 @@ import Joi from 'joi'; import { omit } from 'lodash'; import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; import { CMServerLibs } from '../../lib/lib'; +import { BeatEnrollmentStatus } from '../../lib/lib'; import { wrapEsError } from '../../utils/error_wrappers'; -import { BeatEnrollmentStatus } from './../../lib/lib'; // TODO: add license check pre-hook // TODO: write to Kibana audit log file diff --git a/x-pack/plugins/beats/server/rest_api/beats/list.ts b/x-pack/plugins/beats_management/server/rest_api/beats/list.ts similarity index 100% rename from x-pack/plugins/beats/server/rest_api/beats/list.ts rename to x-pack/plugins/beats_management/server/rest_api/beats/list.ts diff --git a/x-pack/plugins/beats/server/rest_api/beats/tag_assignment.ts b/x-pack/plugins/beats_management/server/rest_api/beats/tag_assignment.ts similarity index 100% rename from x-pack/plugins/beats/server/rest_api/beats/tag_assignment.ts rename to x-pack/plugins/beats_management/server/rest_api/beats/tag_assignment.ts diff --git a/x-pack/plugins/beats/server/rest_api/beats/tag_removal.ts b/x-pack/plugins/beats_management/server/rest_api/beats/tag_removal.ts similarity index 100% rename from x-pack/plugins/beats/server/rest_api/beats/tag_removal.ts rename to x-pack/plugins/beats_management/server/rest_api/beats/tag_removal.ts diff --git a/x-pack/plugins/beats/server/rest_api/beats/update.ts b/x-pack/plugins/beats_management/server/rest_api/beats/update.ts similarity index 100% rename from x-pack/plugins/beats/server/rest_api/beats/update.ts rename to x-pack/plugins/beats_management/server/rest_api/beats/update.ts diff --git a/x-pack/plugins/beats/server/rest_api/tags/set.ts b/x-pack/plugins/beats_management/server/rest_api/tags/set.ts similarity index 100% rename from x-pack/plugins/beats/server/rest_api/tags/set.ts rename to x-pack/plugins/beats_management/server/rest_api/tags/set.ts diff --git a/x-pack/plugins/beats/server/rest_api/tokens/create.ts b/x-pack/plugins/beats_management/server/rest_api/tokens/create.ts similarity index 100% rename from x-pack/plugins/beats/server/rest_api/tokens/create.ts rename to x-pack/plugins/beats_management/server/rest_api/tokens/create.ts diff --git a/x-pack/plugins/beats/server/utils/README.md b/x-pack/plugins/beats_management/server/utils/README.md similarity index 100% rename from x-pack/plugins/beats/server/utils/README.md rename to x-pack/plugins/beats_management/server/utils/README.md diff --git a/x-pack/plugins/beats/server/utils/error_wrappers/index.ts b/x-pack/plugins/beats_management/server/utils/error_wrappers/index.ts similarity index 100% rename from x-pack/plugins/beats/server/utils/error_wrappers/index.ts rename to x-pack/plugins/beats_management/server/utils/error_wrappers/index.ts diff --git a/x-pack/plugins/beats/server/utils/error_wrappers/wrap_es_error.test.ts b/x-pack/plugins/beats_management/server/utils/error_wrappers/wrap_es_error.test.ts similarity index 100% rename from x-pack/plugins/beats/server/utils/error_wrappers/wrap_es_error.test.ts rename to x-pack/plugins/beats_management/server/utils/error_wrappers/wrap_es_error.test.ts diff --git a/x-pack/plugins/beats/server/utils/error_wrappers/wrap_es_error.ts b/x-pack/plugins/beats_management/server/utils/error_wrappers/wrap_es_error.ts similarity index 100% rename from x-pack/plugins/beats/server/utils/error_wrappers/wrap_es_error.ts rename to x-pack/plugins/beats_management/server/utils/error_wrappers/wrap_es_error.ts diff --git a/x-pack/plugins/beats/server/utils/find_non_existent_items.ts b/x-pack/plugins/beats_management/server/utils/find_non_existent_items.ts similarity index 100% rename from x-pack/plugins/beats/server/utils/find_non_existent_items.ts rename to x-pack/plugins/beats_management/server/utils/find_non_existent_items.ts diff --git a/x-pack/plugins/beats/server/utils/index_templates/beats_template.json b/x-pack/plugins/beats_management/server/utils/index_templates/beats_template.json similarity index 100% rename from x-pack/plugins/beats/server/utils/index_templates/beats_template.json rename to x-pack/plugins/beats_management/server/utils/index_templates/beats_template.json diff --git a/x-pack/plugins/beats/server/utils/index_templates/index.ts b/x-pack/plugins/beats_management/server/utils/index_templates/index.ts similarity index 100% rename from x-pack/plugins/beats/server/utils/index_templates/index.ts rename to x-pack/plugins/beats_management/server/utils/index_templates/index.ts diff --git a/x-pack/plugins/beats/server/utils/polyfills.ts b/x-pack/plugins/beats_management/server/utils/polyfills.ts similarity index 100% rename from x-pack/plugins/beats/server/utils/polyfills.ts rename to x-pack/plugins/beats_management/server/utils/polyfills.ts diff --git a/x-pack/plugins/beats/server/utils/wrap_request.ts b/x-pack/plugins/beats_management/server/utils/wrap_request.ts similarity index 100% rename from x-pack/plugins/beats/server/utils/wrap_request.ts rename to x-pack/plugins/beats_management/server/utils/wrap_request.ts diff --git a/x-pack/plugins/beats/tsconfig.json b/x-pack/plugins/beats_management/tsconfig.json similarity index 100% rename from x-pack/plugins/beats/tsconfig.json rename to x-pack/plugins/beats_management/tsconfig.json diff --git a/x-pack/plugins/beats/types/json.t.ts b/x-pack/plugins/beats_management/types/json.t.ts similarity index 100% rename from x-pack/plugins/beats/types/json.t.ts rename to x-pack/plugins/beats_management/types/json.t.ts diff --git a/x-pack/plugins/beats/wallaby.js b/x-pack/plugins/beats_management/wallaby.js similarity index 100% rename from x-pack/plugins/beats/wallaby.js rename to x-pack/plugins/beats_management/wallaby.js diff --git a/x-pack/plugins/index_management/index.js b/x-pack/plugins/index_management/index.js index c3d3e8fe1caf5..9a78cfef48c13 100644 --- a/x-pack/plugins/index_management/index.js +++ b/x-pack/plugins/index_management/index.js @@ -12,15 +12,13 @@ import { registerStatsRoute } from './server/routes/api/stats'; import { registerLicenseChecker } from './server/lib/register_license_checker'; import { PLUGIN } from './common/constants'; -export function indexManagement(kibana) { +export function indexManagement(kibana) { return new kibana.Plugin({ id: PLUGIN.ID, publicDir: resolve(__dirname, 'public'), require: ['kibana', 'elasticsearch', 'xpack_main'], uiExports: { - managementSections: [ - 'plugins/index_management', - ] + managementSections: ['plugins/index_management'], }, init: function (server) { registerLicenseChecker(server); @@ -28,6 +26,6 @@ export function indexManagement(kibana) { registerSettingsRoutes(server); registerStatsRoute(server); registerMappingRoute(server); - } + }, }); } diff --git a/x-pack/yarn.lock b/x-pack/yarn.lock index 3fc10b7dd6909..7d1dc68653e81 100644 --- a/x-pack/yarn.lock +++ b/x-pack/yarn.lock @@ -143,6 +143,10 @@ dependencies: "@types/node" "*" +"@types/history@*", "@types/history@^4.6.2": + version "4.6.2" + resolved "https://registry.yarnpkg.com/@types/history/-/history-4.6.2.tgz#12cfaba693ba20f114ed5765467ff25fdf67ddb0" + "@types/is-stream@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@types/is-stream/-/is-stream-1.1.0.tgz#b84d7bb207a210f2af9bed431dc0fbe9c4143be1" @@ -193,6 +197,27 @@ dependencies: "@types/node" "*" +"@types/react-router-dom@^4.2.7": + version "4.2.7" + resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-4.2.7.tgz#9d36bfe175f916dd8d7b6b0237feed6cce376b4c" + dependencies: + "@types/history" "*" + "@types/react" "*" + "@types/react-router" "*" + +"@types/react-router@*": + version "4.0.29" + resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-4.0.29.tgz#1a906dd99abf21297a5b7cf003d1fd36e7a92069" + dependencies: + "@types/history" "*" + "@types/react" "*" + +"@types/react@*": + version "16.4.6" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.4.6.tgz#5024957c6bcef4f02823accf5974faba2e54fada" + dependencies: + csstype "^2.2.0" + "@types/retry@*", "@types/retry@^0.10.2": version "0.10.2" resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.10.2.tgz#bd1740c4ad51966609b058803ee6874577848b37" @@ -1712,6 +1737,10 @@ cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0": dependencies: cssom "0.3.x" +csstype@^2.2.0: + version "2.5.5" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.5.5.tgz#4125484a3d42189a863943f23b9e4b80fedfa106" + currently-unhandled@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" From d632dca1a96ecd4a09eeff1c6b2495e3b3bd5979 Mon Sep 17 00:00:00 2001 From: Matt Apperson Date: Thu, 26 Jul 2018 11:40:02 -0400 Subject: [PATCH 25/43] Beats/initial ui (#20994) * initial layout and main nav * modal UI and pattern for UI established * fix path * wire up in-memroy adapters * tweak adapters --- .../public/components/layouts/primary.tsx | 43 ++++++ .../plugins/beats_management/public/index.tsx | 10 +- .../lib/adapters/beats/adapter_types.ts | 34 +++++ .../adapters/beats/memory_beats_adapter.ts | 79 ++++++++++++ .../framework/kibana_framework_adapter.ts | 5 +- .../public/lib/adapters/tags/adapter_types.ts | 11 ++ .../lib/adapters/tags/memory_tags_adapter.ts | 30 +++++ .../lib/adapters/tokens/adapter_types.ts | 13 ++ .../adapters/tokens/memory_tokens_adapter.ts | 16 +++ .../public/lib/compose/kibana.ts | 14 +- .../public/lib/compose/memory.ts | 40 ++++++ .../beats_management/public/lib/lib.ts | 12 +- .../public/pages/main/activity.tsx | 13 ++ .../public/pages/main/beats.tsx | 88 +++++++++++++ .../public/pages/main/index.tsx | 122 ++++++++++++++++++ .../public/pages/{home.tsx => main/tags.tsx} | 4 +- .../public/{routes.tsx => router.tsx} | 12 +- .../server/lib/adapters/tags/adapter_types.ts | 2 +- .../lib/adapters/tags/memory_tags_adapter.ts | 2 +- .../adapters/tokens/memory_tokens_adapter.ts | 2 +- .../__tests__/beats/assign_tags.test.ts | 2 +- 21 files changed, 531 insertions(+), 23 deletions(-) create mode 100644 x-pack/plugins/beats_management/public/components/layouts/primary.tsx create mode 100644 x-pack/plugins/beats_management/public/lib/adapters/beats/adapter_types.ts create mode 100644 x-pack/plugins/beats_management/public/lib/adapters/beats/memory_beats_adapter.ts create mode 100644 x-pack/plugins/beats_management/public/lib/adapters/tags/adapter_types.ts create mode 100644 x-pack/plugins/beats_management/public/lib/adapters/tags/memory_tags_adapter.ts create mode 100644 x-pack/plugins/beats_management/public/lib/adapters/tokens/adapter_types.ts create mode 100644 x-pack/plugins/beats_management/public/lib/adapters/tokens/memory_tokens_adapter.ts create mode 100644 x-pack/plugins/beats_management/public/lib/compose/memory.ts create mode 100644 x-pack/plugins/beats_management/public/pages/main/activity.tsx create mode 100644 x-pack/plugins/beats_management/public/pages/main/beats.tsx create mode 100644 x-pack/plugins/beats_management/public/pages/main/index.tsx rename x-pack/plugins/beats_management/public/pages/{home.tsx => main/tags.tsx} (75%) rename x-pack/plugins/beats_management/public/{routes.tsx => router.tsx} (52%) diff --git a/x-pack/plugins/beats_management/public/components/layouts/primary.tsx b/x-pack/plugins/beats_management/public/components/layouts/primary.tsx new file mode 100644 index 0000000000000..cd9e8076dd092 --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/layouts/primary.tsx @@ -0,0 +1,43 @@ +/* + * 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 React from 'react'; +import styled from 'styled-components'; + +import { EuiPage, EuiPageBody, EuiPageContent, EuiPageContentBody, EuiTitle } from '@elastic/eui'; + +interface PrimaryLayoutProps { + title: string; + actionSection: React.ReactNode; +} + +const HeaderContainer = styled.div` + display: flex; + justify-content: space-between; + align-items: flex-start; + padding: 24px 24px 0; + margin-bottom: 16px; +`; + +export const PrimaryLayout: React.SFC = ({ + actionSection, + title, + children, +}) => ( + + + + + +

{title}

+
+ {actionSection} +
+ {children} +
+
+
+); diff --git a/x-pack/plugins/beats_management/public/index.tsx b/x-pack/plugins/beats_management/public/index.tsx index 1334deb1524e6..670aa23f8695b 100644 --- a/x-pack/plugins/beats_management/public/index.tsx +++ b/x-pack/plugins/beats_management/public/index.tsx @@ -6,17 +6,19 @@ import React from 'react'; import { BASE_PATH } from '../common/constants'; -import { compose } from './lib/compose/kibana'; +import { compose } from './lib/compose/memory'; +import { FrontendLibs } from './lib/lib'; + // import * as euiVars from '@elastic/eui/dist/eui_theme_k6_light.json'; // import { ThemeProvider } from 'styled-components'; -import { PageRouter } from './routes'; +import { PageRouter } from './router'; // TODO use theme provided from parentApp when kibana supports it import '@elastic/eui/dist/eui_theme_light.css'; -function startApp(libs: any) { +function startApp(libs: FrontendLibs) { libs.framework.registerManagementSection('beats', 'Beats Management', BASE_PATH); - libs.framework.render(); + libs.framework.render(); } startApp(compose()); diff --git a/x-pack/plugins/beats_management/public/lib/adapters/beats/adapter_types.ts b/x-pack/plugins/beats_management/public/lib/adapters/beats/adapter_types.ts new file mode 100644 index 0000000000000..444ae817088e7 --- /dev/null +++ b/x-pack/plugins/beats_management/public/lib/adapters/beats/adapter_types.ts @@ -0,0 +1,34 @@ +/* + * 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 { CMBeat } from '../../../../common/domain_types'; + +export interface CMBeatsAdapter { + get(id: string): Promise; + getAll(): Promise; + getWithIds(beatIds: string[]): Promise; + removeTagsFromBeats(removals: BeatsTagAssignment[]): Promise; + assignTagsToBeats(assignments: BeatsTagAssignment[]): Promise; +} + +export interface BeatsTagAssignment { + beatId: string; + tag: string; + idxInRequest?: number; +} + +interface BeatsReturnedTagAssignment { + status: number | null; + result?: string; +} + +export interface CMAssignmentReturn { + assignments: BeatsReturnedTagAssignment[]; +} + +export interface BeatsRemovalReturn { + removals: BeatsReturnedTagAssignment[]; +} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/beats/memory_beats_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/beats/memory_beats_adapter.ts new file mode 100644 index 0000000000000..e1929a7d6c69a --- /dev/null +++ b/x-pack/plugins/beats_management/public/lib/adapters/beats/memory_beats_adapter.ts @@ -0,0 +1,79 @@ +/* + * 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 { omit } from 'lodash'; + +import { CMBeat } from '../../../../common/domain_types'; +import { BeatsTagAssignment, CMBeatsAdapter } from './adapter_types'; + +export class MemoryBeatsAdapter implements CMBeatsAdapter { + private beatsDB: CMBeat[]; + + constructor(beatsDB: CMBeat[]) { + this.beatsDB = beatsDB; + } + + public async get(id: string) { + return this.beatsDB.find(beat => beat.id === id) || null; + } + + public async getWithIds(beatIds: string[]) { + return this.beatsDB.filter(beat => beatIds.includes(beat.id)); + } + + public async getAll() { + return this.beatsDB.map((beat: any) => omit(beat, ['access_token'])); + } + + public async removeTagsFromBeats(removals: BeatsTagAssignment[]): Promise { + const beatIds = removals.map(r => r.beatId); + + const response = this.beatsDB.filter(beat => beatIds.includes(beat.id)).map(beat => { + const tagData = removals.find(r => r.beatId === beat.id); + if (tagData) { + if (beat.tags) { + beat.tags = beat.tags.filter(tag => tag !== tagData.tag); + } + } + return beat; + }); + + return response.map((item: CMBeat, resultIdx: number) => ({ + idxInRequest: removals[resultIdx].idxInRequest, + result: 'updated', + status: 200, + })); + } + + public async assignTagsToBeats(assignments: BeatsTagAssignment[]): Promise { + const beatIds = assignments.map(r => r.beatId); + + this.beatsDB.filter(beat => beatIds.includes(beat.id)).map(beat => { + // get tags that need to be assigned to this beat + const tags = assignments + .filter(a => a.beatId === beat.id) + .map((t: BeatsTagAssignment) => t.tag); + + if (tags.length > 0) { + if (!beat.tags) { + beat.tags = []; + } + const nonExistingTags = tags.filter((t: string) => beat.tags && !beat.tags.includes(t)); + + if (nonExistingTags.length > 0) { + beat.tags = beat.tags.concat(nonExistingTags); + } + } + return beat; + }); + + return assignments.map((item: BeatsTagAssignment, resultIdx: number) => ({ + idxInRequest: assignments[resultIdx].idxInRequest, + result: 'updated', + status: 200, + })); + } +} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/framework/kibana_framework_adapter.ts index 042ebd71f9862..1a61a5581ce4e 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/framework/kibana_framework_adapter.ts @@ -86,8 +86,9 @@ export class KibanaFrameworkAdapter implements FrameworkAdapter { private register = (adapterModule: IModule) => { const adapter = this; - this.routes.when(`/management/beats_management/?`, { - template: '
', + this.routes.when(`/management/beats_management/:view?/:id?/:other?/:other2?`, { + template: + '
', controllerAs: 'beatsManagement', // tslint:disable-next-line: max-classes-per-file controller: class BeatsManagementController { diff --git a/x-pack/plugins/beats_management/public/lib/adapters/tags/adapter_types.ts b/x-pack/plugins/beats_management/public/lib/adapters/tags/adapter_types.ts new file mode 100644 index 0000000000000..7fceea057d4f9 --- /dev/null +++ b/x-pack/plugins/beats_management/public/lib/adapters/tags/adapter_types.ts @@ -0,0 +1,11 @@ +/* + * 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 { BeatTag } from '../../../../common/domain_types'; + +export interface CMTagsAdapter { + getTagsWithIds(tagIds: string[]): Promise; + upsertTag(tag: BeatTag): Promise<{}>; +} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/tags/memory_tags_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/tags/memory_tags_adapter.ts new file mode 100644 index 0000000000000..1baf2a65bb46f --- /dev/null +++ b/x-pack/plugins/beats_management/public/lib/adapters/tags/memory_tags_adapter.ts @@ -0,0 +1,30 @@ +/* + * 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 { BeatTag } from '../../../../common/domain_types'; +import { CMTagsAdapter } from './adapter_types'; + +export class MemoryTagsAdapter implements CMTagsAdapter { + private tagsDB: BeatTag[] = []; + + constructor(tagsDB: BeatTag[]) { + this.tagsDB = tagsDB; + } + + public async getTagsWithIds(tagIds: string[]) { + return this.tagsDB.filter(tag => tagIds.includes(tag.id)); + } + + public async upsertTag(tag: BeatTag) { + const existingTagIndex = this.tagsDB.findIndex(t => t.id === tag.id); + if (existingTagIndex !== -1) { + this.tagsDB[existingTagIndex] = tag; + } else { + this.tagsDB.push(tag); + } + return tag; + } +} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/tokens/adapter_types.ts b/x-pack/plugins/beats_management/public/lib/adapters/tokens/adapter_types.ts new file mode 100644 index 0000000000000..43f2d95b02b2c --- /dev/null +++ b/x-pack/plugins/beats_management/public/lib/adapters/tokens/adapter_types.ts @@ -0,0 +1,13 @@ +/* + * 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 interface TokenEnrollmentData { + token: string | null; + expires_on: string; +} + +export interface CMTokensAdapter { + createEnrollmentToken(): Promise; +} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/tokens/memory_tokens_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/tokens/memory_tokens_adapter.ts new file mode 100644 index 0000000000000..4e80bc234d186 --- /dev/null +++ b/x-pack/plugins/beats_management/public/lib/adapters/tokens/memory_tokens_adapter.ts @@ -0,0 +1,16 @@ +/* + * 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 { CMTokensAdapter, TokenEnrollmentData } from './adapter_types'; + +export class MemoryTokensAdapter implements CMTokensAdapter { + public async createEnrollmentToken(): Promise { + return { + token: '2jnwkrhkwuehriauhweair', + expires_on: new Date().toJSON(), + }; + } +} diff --git a/x-pack/plugins/beats_management/public/lib/compose/kibana.ts b/x-pack/plugins/beats_management/public/lib/compose/kibana.ts index 7a98e62c18459..f27f553c9f327 100644 --- a/x-pack/plugins/beats_management/public/lib/compose/kibana.ts +++ b/x-pack/plugins/beats_management/public/lib/compose/kibana.ts @@ -12,18 +12,30 @@ import { uiModules } from 'ui/modules'; // @ts-ignore: path dynamic for kibana import routes from 'ui/routes'; // @ts-ignore: path dynamic for kibana +import { MemoryBeatsAdapter } from '../adapters/beats/memory_beats_adapter'; import { KibanaFrameworkAdapter } from '../adapters/framework/kibana_framework_adapter'; -import { FrontendLibs } from '../lib'; +import { MemoryTagsAdapter } from '../adapters/tags/memory_tags_adapter'; +import { MemoryTokensAdapter } from '../adapters/tokens/memory_tokens_adapter'; +import { FrontendDomainLibs, FrontendLibs } from '../lib'; export function compose(): FrontendLibs { // const kbnVersion = (window as any).__KBN__.version; + const tags = new MemoryTagsAdapter([]); + const tokens = new MemoryTokensAdapter(); + const beats = new MemoryBeatsAdapter([]); + const domainLibs: FrontendDomainLibs = { + tags, + tokens, + beats, + }; const pluginUIModule = uiModules.get('app/beats_management'); const framework = new KibanaFrameworkAdapter(pluginUIModule, management, routes); const libs: FrontendLibs = { framework, + ...domainLibs, }; return libs; } diff --git a/x-pack/plugins/beats_management/public/lib/compose/memory.ts b/x-pack/plugins/beats_management/public/lib/compose/memory.ts new file mode 100644 index 0000000000000..ef65d77229ec9 --- /dev/null +++ b/x-pack/plugins/beats_management/public/lib/compose/memory.ts @@ -0,0 +1,40 @@ +/* + * 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 'ui/autoload/all'; +// @ts-ignore: path dynamic for kibana +import { management } from 'ui/management'; +// @ts-ignore: path dynamic for kibana +import { uiModules } from 'ui/modules'; +// @ts-ignore: path dynamic for kibana +import routes from 'ui/routes'; +// @ts-ignore: path dynamic for kibana +import { MemoryBeatsAdapter } from '../adapters/beats/memory_beats_adapter'; +import { KibanaFrameworkAdapter } from '../adapters/framework/kibana_framework_adapter'; +import { MemoryTagsAdapter } from '../adapters/tags/memory_tags_adapter'; +import { MemoryTokensAdapter } from '../adapters/tokens/memory_tokens_adapter'; + +import { FrontendDomainLibs, FrontendLibs } from '../lib'; + +export function compose(): FrontendLibs { + const tags = new MemoryTagsAdapter([]); + const tokens = new MemoryTokensAdapter(); + const beats = new MemoryBeatsAdapter([]); + + const domainLibs: FrontendDomainLibs = { + tags, + tokens, + beats, + }; + const pluginUIModule = uiModules.get('app/beats_management'); + + const framework = new KibanaFrameworkAdapter(pluginUIModule, management, routes); + const libs: FrontendLibs = { + ...domainLibs, + framework, + }; + return libs; +} diff --git a/x-pack/plugins/beats_management/public/lib/lib.ts b/x-pack/plugins/beats_management/public/lib/lib.ts index 085efc3e53201..03dc74122abcb 100644 --- a/x-pack/plugins/beats_management/public/lib/lib.ts +++ b/x-pack/plugins/beats_management/public/lib/lib.ts @@ -7,10 +7,18 @@ import { IModule, IScope } from 'angular'; import { AxiosRequestConfig } from 'axios'; import React from 'react'; +import { CMBeatsAdapter } from './adapters/beats/adapter_types'; +import { CMTagsAdapter } from './adapters/tags/adapter_types'; +import { CMTokensAdapter } from './adapters/tokens/adapter_types'; -export interface FrontendLibs { +export interface FrontendDomainLibs { + beats: CMBeatsAdapter; + tags: CMTagsAdapter; + tokens: CMTokensAdapter; +} + +export interface FrontendLibs extends FrontendDomainLibs { framework: FrameworkAdapter; - // api: ApiAdapter; } export interface FrameworkAdapter { diff --git a/x-pack/plugins/beats_management/public/pages/main/activity.tsx b/x-pack/plugins/beats_management/public/pages/main/activity.tsx new file mode 100644 index 0000000000000..5aa523e3a32f8 --- /dev/null +++ b/x-pack/plugins/beats_management/public/pages/main/activity.tsx @@ -0,0 +1,13 @@ +/* + * 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 React from 'react'; + +export class ActivityPage extends React.PureComponent { + public render() { + return
activity logs view
; + } +} diff --git a/x-pack/plugins/beats_management/public/pages/main/beats.tsx b/x-pack/plugins/beats_management/public/pages/main/beats.tsx new file mode 100644 index 0000000000000..a8c7518a3e27c --- /dev/null +++ b/x-pack/plugins/beats_management/public/pages/main/beats.tsx @@ -0,0 +1,88 @@ +/* + * 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 { + EuiButton, + EuiButtonEmpty, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiOverlayMask, +} from '@elastic/eui'; + +import { CMBeat } from '../../../common/domain_types'; +import { FrontendLibs } from '../../lib/lib'; + +import React from 'react'; +interface BeatsPageProps { + libs: FrontendLibs; +} + +interface BeatsPageState { + beats: CMBeat[]; +} + +export class BeatsPage extends React.PureComponent { + public static ActionArea = ({ match, history }: { match: any; history: any }) => ( +
+ { + window.alert('This will later go to more general beats install instructions.'); + window.location.href = '#/home/tutorial/dockerMetrics'; + }} + > + Learn how to install beats + + { + history.push('/beats/enroll/foobar'); + }} + > + Enroll Beats + + + {match.params.enrollmentToken != null && ( + + history.push('/beats')} style={{ width: '800px' }}> + + Enroll Beats + + + + Enrollment UI here for enrollment token of: {match.params.enrollmentToken} + + + + history.push('/beats')}>Cancel + + + + )} +
+ ); + constructor(props: BeatsPageProps) { + super(props); + + this.state = { + beats: [], + }; + + this.loadBeats(); + } + public render() { + return
beats table and stuff - {this.state.beats.length}
; + } + private async loadBeats() { + const beats = await this.props.libs.beats.getAll(); + this.setState({ + beats, + }); + } +} diff --git a/x-pack/plugins/beats_management/public/pages/main/index.tsx b/x-pack/plugins/beats_management/public/pages/main/index.tsx new file mode 100644 index 0000000000000..f18e5e05b5747 --- /dev/null +++ b/x-pack/plugins/beats_management/public/pages/main/index.tsx @@ -0,0 +1,122 @@ +/* + * 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 { + // @ts-ignore + EuiTab, + // @ts-ignore + EuiTabs, +} from '@elastic/eui'; +import React from 'react'; +import { Redirect, Route, Switch } from 'react-router-dom'; +import { PrimaryLayout } from '../../components/layouts/primary'; +import { FrontendLibs } from '../../lib/lib'; +import { ActivityPage } from './activity'; +import { BeatsPage } from './beats'; +import { TagsPage } from './tags'; + +interface MainPagesProps { + history: any; + libs: FrontendLibs; +} + +interface MainPagesState { + selectedTabId: string; + enrollBeat?: { + enrollmentToken: string; + } | null; +} + +export class MainPages extends React.PureComponent { + constructor(props: any) { + super(props); + + this.state = { + selectedTabId: '/', + }; + } + public toggleEnrollBeat = () => { + if (this.state.enrollBeat) { + return this.setState({ + enrollBeat: null, + }); + } + + // TODO: create a real enromment token + return this.setState({ + enrollBeat: { enrollmentToken: '5g3i4ug5uy34g' }, + }); + }; + + public onSelectedTabChanged = (id: string) => { + this.props.history.push(id); + }; + + public render() { + const tabs = [ + { + id: '/beats', + name: 'Beats List', + disabled: false, + }, + { + id: '/activity', + name: 'Beats Activity', + disabled: false, + }, + { + id: '/tags', + name: 'Tags', + disabled: false, + }, + ]; + + const renderedTabs = tabs.map((tab, index) => ( + this.onSelectedTabChanged(tab.id)} + isSelected={tab.id === this.props.history.location.pathname} + disabled={tab.disabled} + key={index} + > + {tab.name} + + )); + return ( + + + + } + > + {renderedTabs} + + + } + /> + } + /> + } + /> + } + /> + + + ); + } +} diff --git a/x-pack/plugins/beats_management/public/pages/home.tsx b/x-pack/plugins/beats_management/public/pages/main/tags.tsx similarity index 75% rename from x-pack/plugins/beats_management/public/pages/home.tsx rename to x-pack/plugins/beats_management/public/pages/main/tags.tsx index 505015f18af16..66dab3c1b3550 100644 --- a/x-pack/plugins/beats_management/public/pages/home.tsx +++ b/x-pack/plugins/beats_management/public/pages/main/tags.tsx @@ -6,8 +6,8 @@ import React from 'react'; -export class HomePage extends React.PureComponent { +export class TagsPage extends React.PureComponent { public render() { - return
Home
; + return
tags table and stuff
; } } diff --git a/x-pack/plugins/beats_management/public/routes.tsx b/x-pack/plugins/beats_management/public/router.tsx similarity index 52% rename from x-pack/plugins/beats_management/public/routes.tsx rename to x-pack/plugins/beats_management/public/router.tsx index f5863b28aaafa..7fb283c33da86 100644 --- a/x-pack/plugins/beats_management/public/routes.tsx +++ b/x-pack/plugins/beats_management/public/router.tsx @@ -5,18 +5,14 @@ */ import React from 'react'; -import { HashRouter, Route, Switch } from 'react-router-dom'; +import { HashRouter, Route } from 'react-router-dom'; -import { NotFoundPage } from './pages/404'; -import { HomePage } from './pages/home'; +import { MainPages } from './pages/main'; -export const PageRouter: React.SFC<{}> = () => { +export const PageRouter: React.SFC<{ libs: any }> = ({ libs }) => { return ( - - - - + } /> ); }; diff --git a/x-pack/plugins/beats_management/server/lib/adapters/tags/adapter_types.ts b/x-pack/plugins/beats_management/server/lib/adapters/tags/adapter_types.ts index 19333c831d594..7bbb42d415f52 100644 --- a/x-pack/plugins/beats_management/server/lib/adapters/tags/adapter_types.ts +++ b/x-pack/plugins/beats_management/server/lib/adapters/tags/adapter_types.ts @@ -7,6 +7,6 @@ import { BeatTag } from '../../../../common/domain_types'; import { FrameworkUser } from '../framework/adapter_types'; export interface CMTagsAdapter { - getTagsWithIds(user: FrameworkUser, tagIds: string[]): any; + getTagsWithIds(user: FrameworkUser, tagIds: string[]): Promise; upsertTag(user: FrameworkUser, tag: BeatTag): Promise<{}>; } diff --git a/x-pack/plugins/beats_management/server/lib/adapters/tags/memory_tags_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/tags/memory_tags_adapter.ts index 64ef401008ae1..c3470011f6a4e 100644 --- a/x-pack/plugins/beats_management/server/lib/adapters/tags/memory_tags_adapter.ts +++ b/x-pack/plugins/beats_management/server/lib/adapters/tags/memory_tags_adapter.ts @@ -5,7 +5,7 @@ */ import { BeatTag } from '../../../../common/domain_types'; -import { FrameworkUser } from './../framework/adapter_types'; +import { FrameworkUser } from '../framework/adapter_types'; import { CMTagsAdapter } from './adapter_types'; export class MemoryTagsAdapter implements CMTagsAdapter { diff --git a/x-pack/plugins/beats_management/server/lib/adapters/tokens/memory_tokens_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/tokens/memory_tokens_adapter.ts index 7cf132bb6ba31..767f161615ef8 100644 --- a/x-pack/plugins/beats_management/server/lib/adapters/tokens/memory_tokens_adapter.ts +++ b/x-pack/plugins/beats_management/server/lib/adapters/tokens/memory_tokens_adapter.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FrameworkAuthenticatedUser } from './../framework/adapter_types'; +import { FrameworkAuthenticatedUser } from '../framework/adapter_types'; import { CMTokensAdapter, TokenEnrollmentData } from './adapter_types'; export class MemoryTokensAdapter implements CMTokensAdapter { diff --git a/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/assign_tags.test.ts b/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/assign_tags.test.ts index c9f9f15256d43..d0d888d2d3d1f 100644 --- a/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/assign_tags.test.ts +++ b/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/assign_tags.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FrameworkInternalUser } from './../../../adapters/framework/adapter_types'; +import { FrameworkInternalUser } from '../../../adapters/framework/adapter_types'; import { MemoryBeatsAdapter } from '../../../adapters/beats/memory_beats_adapter'; import { TestingBackendFrameworkAdapter } from '../../../adapters/framework/testing_framework_adapter'; From 4025917971b7b7b7da4017204efe523c87082423 Mon Sep 17 00:00:00 2001 From: Matt Apperson Date: Thu, 26 Jul 2018 12:40:21 -0400 Subject: [PATCH 26/43] add getAll method to tags adapter (#21287) --- .../public/lib/adapters/tags/adapter_types.ts | 1 + .../public/lib/adapters/tags/memory_tags_adapter.ts | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/x-pack/plugins/beats_management/public/lib/adapters/tags/adapter_types.ts b/x-pack/plugins/beats_management/public/lib/adapters/tags/adapter_types.ts index 7fceea057d4f9..ab0931ab597bf 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/tags/adapter_types.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/tags/adapter_types.ts @@ -7,5 +7,6 @@ import { BeatTag } from '../../../../common/domain_types'; export interface CMTagsAdapter { getTagsWithIds(tagIds: string[]): Promise; + getAll(): Promise; upsertTag(tag: BeatTag): Promise<{}>; } diff --git a/x-pack/plugins/beats_management/public/lib/adapters/tags/memory_tags_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/tags/memory_tags_adapter.ts index 1baf2a65bb46f..6b7ddd1c98e8c 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/tags/memory_tags_adapter.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/tags/memory_tags_adapter.ts @@ -18,6 +18,10 @@ export class MemoryTagsAdapter implements CMTagsAdapter { return this.tagsDB.filter(tag => tagIds.includes(tag.id)); } + public async getAll() { + return this.tagsDB; + } + public async upsertTag(tag: BeatTag) { const existingTagIndex = this.tagsDB.findIndex(t => t.id === tag.id); if (existingTagIndex !== -1) { From 87a8991cacd75d20f1e1f7d2aef4fa6c8a2fc9bb Mon Sep 17 00:00:00 2001 From: Matt Apperson Date: Thu, 2 Aug 2018 15:10:47 -0400 Subject: [PATCH 27/43] Beats/real adapters (#21481) * add initial real adapters, and nulled data where we need endpoints * UI adapters and needed endpoints added (though not tested) * prep for route tests and some cleanup * move files --- .../lib/adapters/beats/adapter_types.ts | 5 +- .../adapters/beats/memory_beats_adapter.ts | 15 +-- .../lib/adapters/beats/rest_beats_adapter.ts | 37 ++++++ .../lib/adapters/rest_api/adapter_types.ts | 12 ++ .../rest_api/axios_rest_api_adapter.ts | 81 ++++++++++++ .../public/lib/adapters/tags/adapter_types.ts | 2 +- .../lib/adapters/tags/rest_tags_adapter.ts | 27 ++++ .../adapters/tokens/rest_tokens_adapter.ts | 17 +++ .../public/lib/compose/kibana.ts | 19 +-- .../lib/adapters/beats/adapter_types.ts | 2 +- .../beats/elasticsearch_beats_adapter.ts | 4 +- .../adapters/beats/memory_beats_adapter.ts | 2 +- .../framework/testing_framework_adapter.ts | 34 +----- .../server/lib/adapters/tags/adapter_types.ts | 1 + .../tags/elasticsearch_tags_adapter.ts | 11 ++ .../lib/adapters/tags/memory_tags_adapter.ts | 4 + .../server/lib/compose/testing.ts | 53 ++++++++ .../__tests__/beats/assign_tags.test.ts | 2 +- .../domains/__tests__/beats/enroll.test.ts | 2 +- .../__tests__/beats/remove_tags.test.ts | 100 +++++++++++++++ .../lib/domains/__tests__/tokens.test.ts | 2 +- .../server/lib/domains/beats.ts | 20 +-- .../server/lib/domains/tags.ts | 4 + .../beats_management/server/lib/lib.ts | 2 +- .../server/management_server.ts | 10 +- .../server/rest_api/beats/configuration.ts | 2 +- .../server/rest_api/beats/enroll.ts | 4 +- .../server/rest_api/beats/get.ts | 39 ++++++ .../server/rest_api/beats/list.ts | 4 +- .../server/rest_api/beats/tag_assignment.ts | 18 ++- .../server/rest_api/beats/tag_removal.ts | 4 +- .../server/rest_api/beats/update.ts | 4 +- .../server/rest_api/tags/get.ts | 27 ++++ .../server/rest_api/tags/list.ts | 24 ++++ .../server/rest_api/tags/set.ts | 4 +- .../server/rest_api/tokens/create.ts | 4 +- .../apis/beats/assign_tags_to_beats.js | 115 +++++++----------- 37 files changed, 554 insertions(+), 163 deletions(-) create mode 100644 x-pack/plugins/beats_management/public/lib/adapters/beats/rest_beats_adapter.ts create mode 100644 x-pack/plugins/beats_management/public/lib/adapters/rest_api/adapter_types.ts create mode 100644 x-pack/plugins/beats_management/public/lib/adapters/rest_api/axios_rest_api_adapter.ts create mode 100644 x-pack/plugins/beats_management/public/lib/adapters/tags/rest_tags_adapter.ts create mode 100644 x-pack/plugins/beats_management/public/lib/adapters/tokens/rest_tokens_adapter.ts create mode 100644 x-pack/plugins/beats_management/server/lib/compose/testing.ts create mode 100644 x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/remove_tags.test.ts create mode 100644 x-pack/plugins/beats_management/server/rest_api/beats/get.ts create mode 100644 x-pack/plugins/beats_management/server/rest_api/tags/get.ts create mode 100644 x-pack/plugins/beats_management/server/rest_api/tags/list.ts diff --git a/x-pack/plugins/beats_management/public/lib/adapters/beats/adapter_types.ts b/x-pack/plugins/beats_management/public/lib/adapters/beats/adapter_types.ts index 444ae817088e7..b703edb971299 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/beats/adapter_types.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/beats/adapter_types.ts @@ -9,9 +9,8 @@ import { CMBeat } from '../../../../common/domain_types'; export interface CMBeatsAdapter { get(id: string): Promise; getAll(): Promise; - getWithIds(beatIds: string[]): Promise; - removeTagsFromBeats(removals: BeatsTagAssignment[]): Promise; - assignTagsToBeats(assignments: BeatsTagAssignment[]): Promise; + removeTagsFromBeats(removals: BeatsTagAssignment[]): Promise; + assignTagsToBeats(assignments: BeatsTagAssignment[]): Promise; } export interface BeatsTagAssignment { diff --git a/x-pack/plugins/beats_management/public/lib/adapters/beats/memory_beats_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/beats/memory_beats_adapter.ts index e1929a7d6c69a..1940ec2ada4b0 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/beats/memory_beats_adapter.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/beats/memory_beats_adapter.ts @@ -7,7 +7,12 @@ import { omit } from 'lodash'; import { CMBeat } from '../../../../common/domain_types'; -import { BeatsTagAssignment, CMBeatsAdapter } from './adapter_types'; +import { + BeatsRemovalReturn, + BeatsTagAssignment, + CMAssignmentReturn, + CMBeatsAdapter, +} from './adapter_types'; export class MemoryBeatsAdapter implements CMBeatsAdapter { private beatsDB: CMBeat[]; @@ -20,15 +25,11 @@ export class MemoryBeatsAdapter implements CMBeatsAdapter { return this.beatsDB.find(beat => beat.id === id) || null; } - public async getWithIds(beatIds: string[]) { - return this.beatsDB.filter(beat => beatIds.includes(beat.id)); - } - public async getAll() { return this.beatsDB.map((beat: any) => omit(beat, ['access_token'])); } - public async removeTagsFromBeats(removals: BeatsTagAssignment[]): Promise { + public async removeTagsFromBeats(removals: BeatsTagAssignment[]): Promise { const beatIds = removals.map(r => r.beatId); const response = this.beatsDB.filter(beat => beatIds.includes(beat.id)).map(beat => { @@ -48,7 +49,7 @@ export class MemoryBeatsAdapter implements CMBeatsAdapter { })); } - public async assignTagsToBeats(assignments: BeatsTagAssignment[]): Promise { + public async assignTagsToBeats(assignments: BeatsTagAssignment[]): Promise { const beatIds = assignments.map(r => r.beatId); this.beatsDB.filter(beat => beatIds.includes(beat.id)).map(beat => { diff --git a/x-pack/plugins/beats_management/public/lib/adapters/beats/rest_beats_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/beats/rest_beats_adapter.ts new file mode 100644 index 0000000000000..9da760be307db --- /dev/null +++ b/x-pack/plugins/beats_management/public/lib/adapters/beats/rest_beats_adapter.ts @@ -0,0 +1,37 @@ +/* + * 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 { CMBeat } from '../../../../common/domain_types'; +import { RestAPIAdapter } from '../rest_api/adapter_types'; +import { + BeatsRemovalReturn, + BeatsTagAssignment, + CMAssignmentReturn, + CMBeatsAdapter, +} from './adapter_types'; +export class RestBeatsAdapter implements CMBeatsAdapter { + constructor(private readonly REST: RestAPIAdapter) {} + + public async get(id: string): Promise { + return await this.REST.get(`/api/beats/agent/${id}`); + } + + public async getAll(): Promise { + return await this.REST.get('/api/beats/agents'); + } + + public async removeTagsFromBeats(removals: BeatsTagAssignment[]): Promise { + return await this.REST.post(`/api/beats/agents_tags/removals`, { + removals, + }); + } + + public async assignTagsToBeats(assignments: BeatsTagAssignment[]): Promise { + return await this.REST.post(`/api/beats/agents_tags/assignments`, { + assignments, + }); + } +} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/rest_api/adapter_types.ts b/x-pack/plugins/beats_management/public/lib/adapters/rest_api/adapter_types.ts new file mode 100644 index 0000000000000..222807e7f6948 --- /dev/null +++ b/x-pack/plugins/beats_management/public/lib/adapters/rest_api/adapter_types.ts @@ -0,0 +1,12 @@ +/* + * 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 interface RestAPIAdapter { + get(url: string): Promise; + post(url: string, body?: { [key: string]: any }): Promise; + delete(url: string): Promise; + put(url: string, body?: any): Promise; +} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/rest_api/axios_rest_api_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/rest_api/axios_rest_api_adapter.ts new file mode 100644 index 0000000000000..b05f2b41e30b3 --- /dev/null +++ b/x-pack/plugins/beats_management/public/lib/adapters/rest_api/axios_rest_api_adapter.ts @@ -0,0 +1,81 @@ +/* + * 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 axios, { AxiosInstance } from 'axios'; +import { RestAPIAdapter } from './adapter_types'; +let globalAPI: AxiosInstance; + +export class AxiosRestAPIAdapter implements RestAPIAdapter { + constructor( + private readonly kbnVersion: string, + private readonly xsrfToken: string, + private readonly basePath: string + ) {} + + public async get(url: string): Promise { + return await this.REST.get(url).then(resp => resp.data); + } + + public async post( + url: string, + body?: { [key: string]: any } + ): Promise { + return await this.REST.post(url, body).then(resp => resp.data); + } + + public async delete(url: string): Promise { + return await this.REST.delete(url).then(resp => resp.data); + } + + public async put(url: string, body?: any): Promise { + return await this.REST.put(url, body).then(resp => resp.data); + } + + private get REST() { + if (globalAPI) { + return globalAPI; + } + + globalAPI = axios.create({ + baseURL: this.basePath, + withCredentials: true, + responseType: 'json', + timeout: 30000, + headers: { + Accept: 'application/json', + credentials: 'same-origin', + 'Content-Type': 'application/json', + 'kbn-version': this.kbnVersion, + 'kbn-xsrf': this.xsrfToken, + }, + }); + // Add a request interceptor + globalAPI.interceptors.request.use( + config => { + // Do something before request is sent + return config; + }, + error => { + // Do something with request error + return Promise.reject(error); + } + ); + + // Add a response interceptor + globalAPI.interceptors.response.use( + response => { + // Do something with response data + return response; + }, + error => { + // Do something with response error + return Promise.reject(error); + } + ); + + return globalAPI; + } +} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/tags/adapter_types.ts b/x-pack/plugins/beats_management/public/lib/adapters/tags/adapter_types.ts index ab0931ab597bf..57bdf2592baa3 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/tags/adapter_types.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/tags/adapter_types.ts @@ -8,5 +8,5 @@ import { BeatTag } from '../../../../common/domain_types'; export interface CMTagsAdapter { getTagsWithIds(tagIds: string[]): Promise; getAll(): Promise; - upsertTag(tag: BeatTag): Promise<{}>; + upsertTag(tag: BeatTag): Promise; } diff --git a/x-pack/plugins/beats_management/public/lib/adapters/tags/rest_tags_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/tags/rest_tags_adapter.ts new file mode 100644 index 0000000000000..5eb16762ba825 --- /dev/null +++ b/x-pack/plugins/beats_management/public/lib/adapters/tags/rest_tags_adapter.ts @@ -0,0 +1,27 @@ +/* + * 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 { BeatTag } from '../../../../common/domain_types'; +import { RestAPIAdapter } from '../rest_api/adapter_types'; +import { CMTagsAdapter } from './adapter_types'; + +export class RestTagsAdapter implements CMTagsAdapter { + constructor(private readonly REST: RestAPIAdapter) {} + + public async getTagsWithIds(tagIds: string[]): Promise { + return await this.REST.get(`/api/beats/tags/${tagIds.join(',')}`); + } + + public async getAll(): Promise { + return await this.REST.get(`/api/beats/tags`); + } + + public async upsertTag(tag: BeatTag): Promise { + return await this.REST.put(`/api/beats/tag/{tag}`, { + configuration_blocks: tag.configuration_blocks, + }); + } +} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/tokens/rest_tokens_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/tokens/rest_tokens_adapter.ts new file mode 100644 index 0000000000000..de5890fdaebf5 --- /dev/null +++ b/x-pack/plugins/beats_management/public/lib/adapters/tokens/rest_tokens_adapter.ts @@ -0,0 +1,17 @@ +/* + * 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 { RestAPIAdapter } from '../rest_api/adapter_types'; +import { CMTokensAdapter, TokenEnrollmentData } from './adapter_types'; + +export class RestTokensAdapter implements CMTokensAdapter { + constructor(private readonly REST: RestAPIAdapter) {} + + public async createEnrollmentToken(): Promise { + const tokens = await this.REST.post('/api/beats/enrollment_tokens'); + return tokens[0]; + } +} diff --git a/x-pack/plugins/beats_management/public/lib/compose/kibana.ts b/x-pack/plugins/beats_management/public/lib/compose/kibana.ts index f27f553c9f327..9f8f183170fd1 100644 --- a/x-pack/plugins/beats_management/public/lib/compose/kibana.ts +++ b/x-pack/plugins/beats_management/public/lib/compose/kibana.ts @@ -6,23 +6,28 @@ import 'ui/autoload/all'; // @ts-ignore: path dynamic for kibana +import chrome from 'ui/chrome'; +// @ts-ignore: path dynamic for kibana import { management } from 'ui/management'; // @ts-ignore: path dynamic for kibana import { uiModules } from 'ui/modules'; // @ts-ignore: path dynamic for kibana import routes from 'ui/routes'; // @ts-ignore: path dynamic for kibana -import { MemoryBeatsAdapter } from '../adapters/beats/memory_beats_adapter'; +import { RestBeatsAdapter } from '../adapters/beats/rest_beats_adapter'; import { KibanaFrameworkAdapter } from '../adapters/framework/kibana_framework_adapter'; -import { MemoryTagsAdapter } from '../adapters/tags/memory_tags_adapter'; -import { MemoryTokensAdapter } from '../adapters/tokens/memory_tokens_adapter'; +import { AxiosRestAPIAdapter } from '../adapters/rest_api/axios_rest_api_adapter'; +import { RestTagsAdapter } from '../adapters/tags/rest_tags_adapter'; +import { RestTokensAdapter } from '../adapters/tokens/rest_tokens_adapter'; import { FrontendDomainLibs, FrontendLibs } from '../lib'; export function compose(): FrontendLibs { - // const kbnVersion = (window as any).__KBN__.version; - const tags = new MemoryTagsAdapter([]); - const tokens = new MemoryTokensAdapter(); - const beats = new MemoryBeatsAdapter([]); + const kbnVersion = (window as any).__KBN__.version; + const api = new AxiosRestAPIAdapter(kbnVersion, chrome.getXsrfToken(), chrome.getBasePath()); + + const tags = new RestTagsAdapter(api); + const tokens = new RestTokensAdapter(api); + const beats = new RestBeatsAdapter(api); const domainLibs: FrontendDomainLibs = { tags, diff --git a/x-pack/plugins/beats_management/server/lib/adapters/beats/adapter_types.ts b/x-pack/plugins/beats_management/server/lib/adapters/beats/adapter_types.ts index 8812d9d39bd41..0d68fd528d42a 100644 --- a/x-pack/plugins/beats_management/server/lib/adapters/beats/adapter_types.ts +++ b/x-pack/plugins/beats_management/server/lib/adapters/beats/adapter_types.ts @@ -11,7 +11,7 @@ import { FrameworkUser } from '../framework/adapter_types'; export interface CMBeatsAdapter { insert(beat: CMBeat): Promise; update(beat: CMBeat): Promise; - get(id: string): any; + get(user: FrameworkUser, id: string): any; getAll(user: FrameworkUser): any; getWithIds(user: FrameworkUser, beatIds: string[]): any; removeTagsFromBeats( diff --git a/x-pack/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts index 9a3d067ffa35f..1d00f95e3a4e0 100644 --- a/x-pack/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts +++ b/x-pack/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts @@ -21,7 +21,7 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { this.framework = framework; } - public async get(id: string) { + public async get(user: FrameworkUser, id: string) { const params = { id: `beat:${id}`, ignore: [404], @@ -29,7 +29,7 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { type: '_doc', }; - const response = await this.database.get(this.framework.internalUser, params); + const response = await this.database.get(user, params); if (!response.found) { return null; } diff --git a/x-pack/plugins/beats_management/server/lib/adapters/beats/memory_beats_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/beats/memory_beats_adapter.ts index 1762ff93fc185..1de1fe71e54ea 100644 --- a/x-pack/plugins/beats_management/server/lib/adapters/beats/memory_beats_adapter.ts +++ b/x-pack/plugins/beats_management/server/lib/adapters/beats/memory_beats_adapter.ts @@ -17,7 +17,7 @@ export class MemoryBeatsAdapter implements CMBeatsAdapter { this.beatsDB = beatsDB; } - public async get(id: string) { + public async get(user: FrameworkUser, id: string) { return this.beatsDB.find(beat => beat.id === id) || null; } diff --git a/x-pack/plugins/beats_management/server/lib/adapters/framework/testing_framework_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/framework/testing_framework_adapter.ts index 3280220893892..5462bd36b48ae 100644 --- a/x-pack/plugins/beats_management/server/lib/adapters/framework/testing_framework_adapter.ts +++ b/x-pack/plugins/beats_management/server/lib/adapters/framework/testing_framework_adapter.ts @@ -3,13 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Client } from 'elasticsearch'; -import { get } from 'lodash'; + import { FrameworkInternalUser } from './adapter_types'; import { BackendFrameworkAdapter, - FrameworkRequest, FrameworkRouteOptions, FrameworkWrappableRequest, } from './adapter_types'; @@ -24,15 +22,15 @@ export class TestingBackendFrameworkAdapter implements BackendFrameworkAdapter { kind: 'internal', }; public version: string; - private client: Client | null; private settings: TestSettings; - constructor(client: Client | null, settings: TestSettings) { - this.client = client; - this.settings = settings || { + constructor( + settings: TestSettings = { encryptionKey: 'something_who_cares', enrollmentTokensTtlInSeconds: 10 * 60, // 10 minutes - }; + } + ) { + this.settings = settings; this.version = 'testing'; } @@ -54,24 +52,4 @@ export class TestingBackendFrameworkAdapter implements BackendFrameworkAdapter { ) { // not yet testable } - - public installIndexTemplate(name: string, template: {}) { - return; - } - - public async callWithInternalUser(esMethod: string, options: {}) { - const api = get(this.client, esMethod); - - api(options); - - return await api(options); - } - - public async callWithRequest(req: FrameworkRequest, esMethod: string, options: {}) { - const api = get(this.client, esMethod); - - api(options); - - return await api(options); - } } diff --git a/x-pack/plugins/beats_management/server/lib/adapters/tags/adapter_types.ts b/x-pack/plugins/beats_management/server/lib/adapters/tags/adapter_types.ts index 7bbb42d415f52..77ae4ff8ad095 100644 --- a/x-pack/plugins/beats_management/server/lib/adapters/tags/adapter_types.ts +++ b/x-pack/plugins/beats_management/server/lib/adapters/tags/adapter_types.ts @@ -7,6 +7,7 @@ import { BeatTag } from '../../../../common/domain_types'; import { FrameworkUser } from '../framework/adapter_types'; export interface CMTagsAdapter { + getAll(user: FrameworkUser): Promise; getTagsWithIds(user: FrameworkUser, tagIds: string[]): Promise; upsertTag(user: FrameworkUser, tag: BeatTag): Promise<{}>; } diff --git a/x-pack/plugins/beats_management/server/lib/adapters/tags/elasticsearch_tags_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/tags/elasticsearch_tags_adapter.ts index 2c2c988189283..645ed37f5ff8f 100644 --- a/x-pack/plugins/beats_management/server/lib/adapters/tags/elasticsearch_tags_adapter.ts +++ b/x-pack/plugins/beats_management/server/lib/adapters/tags/elasticsearch_tags_adapter.ts @@ -19,6 +19,17 @@ export class ElasticsearchTagsAdapter implements CMTagsAdapter { this.database = database; } + public async getAll(user: FrameworkUser) { + const params = { + index: INDEX_NAMES.BEATS, + q: 'type:tag', + type: '_doc', + }; + const response = await this.database.search(user, params); + + return get(response, 'hits.hits', []); + } + public async getTagsWithIds(user: FrameworkUser, tagIds: string[]) { const ids = tagIds.map(tag => `tag:${tag}`); diff --git a/x-pack/plugins/beats_management/server/lib/adapters/tags/memory_tags_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/tags/memory_tags_adapter.ts index c3470011f6a4e..7623c8aaa21d8 100644 --- a/x-pack/plugins/beats_management/server/lib/adapters/tags/memory_tags_adapter.ts +++ b/x-pack/plugins/beats_management/server/lib/adapters/tags/memory_tags_adapter.ts @@ -15,6 +15,10 @@ export class MemoryTagsAdapter implements CMTagsAdapter { this.tagsDB = tagsDB; } + public async getAll(user: FrameworkUser) { + return this.tagsDB; + } + public async getTagsWithIds(user: FrameworkUser, tagIds: string[]) { return this.tagsDB.filter(tag => tagIds.includes(tag.id)); } diff --git a/x-pack/plugins/beats_management/server/lib/compose/testing.ts b/x-pack/plugins/beats_management/server/lib/compose/testing.ts new file mode 100644 index 0000000000000..25b6776b6908b --- /dev/null +++ b/x-pack/plugins/beats_management/server/lib/compose/testing.ts @@ -0,0 +1,53 @@ +/* + * 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 { MemoryBeatsAdapter } from '../adapters/beats/memory_beats_adapter'; +import { MemoryTagsAdapter } from '../adapters/tags/memory_tags_adapter'; +import { MemoryTokensAdapter } from '../adapters/tokens/memory_tokens_adapter'; + +import { TestingBackendFrameworkAdapter } from '../adapters/framework/testing_framework_adapter'; + +import { CMBeatsDomain } from '../domains/beats'; +import { CMTagsDomain } from '../domains/tags'; +import { CMTokensDomain } from '../domains/tokens'; + +import { BeatTag, CMBeat } from '../../../common/domain_types'; +import { TokenEnrollmentData } from '../adapters/tokens/adapter_types'; +import { CMDomainLibs, CMServerLibs } from '../lib'; + +export function compose({ + tagsDB = [], + tokensDB = [], + beatsDB = [], +}: { + tagsDB?: BeatTag[]; + tokensDB?: TokenEnrollmentData[]; + beatsDB?: CMBeat[]; +}): CMServerLibs { + const framework = new TestingBackendFrameworkAdapter(); + + const tags = new CMTagsDomain(new MemoryTagsAdapter(tagsDB)); + const tokens = new CMTokensDomain(new MemoryTokensAdapter(tokensDB), { + framework, + }); + const beats = new CMBeatsDomain(new MemoryBeatsAdapter(beatsDB), { + tags, + tokens, + }); + + const domainLibs: CMDomainLibs = { + beats, + tags, + tokens, + }; + + const libs: CMServerLibs = { + framework, + ...domainLibs, + }; + + return libs; +} diff --git a/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/assign_tags.test.ts b/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/assign_tags.test.ts index d0d888d2d3d1f..20f4a7d36e4f0 100644 --- a/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/assign_tags.test.ts +++ b/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/assign_tags.test.ts @@ -82,7 +82,7 @@ describe('Beats Domain Lib', () => { id: 'qa', }, ]; - const framework = new TestingBackendFrameworkAdapter(null, settings); + const framework = new TestingBackendFrameworkAdapter(settings); const tokensLib = new CMTokensDomain(new MemoryTokensAdapter([]), { framework, diff --git a/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/enroll.test.ts b/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/enroll.test.ts index 9f42ad3c89f86..c5bc0935dc6cc 100644 --- a/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/enroll.test.ts +++ b/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/enroll.test.ts @@ -70,7 +70,7 @@ describe('Beats Domain Lib', () => { version, }; - const framework = new TestingBackendFrameworkAdapter(null, settings); + const framework = new TestingBackendFrameworkAdapter(settings); tokensLib = new CMTokensDomain(new MemoryTokensAdapter(tokensDB), { framework, diff --git a/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/remove_tags.test.ts b/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/remove_tags.test.ts new file mode 100644 index 0000000000000..f75334e917c2b --- /dev/null +++ b/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/remove_tags.test.ts @@ -0,0 +1,100 @@ +/* + * 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 { BeatTag, CMBeat } from '../../../../../common/domain_types'; +import { compose } from '../../../compose/testing'; +import { CMServerLibs } from '../../../lib'; +import { FrameworkInternalUser } from './../../../adapters/framework/adapter_types'; + +const internalUser: FrameworkInternalUser = { kind: 'internal' }; + +describe('Beats Domain Lib', () => { + let libs: CMServerLibs; + let beatsDB: CMBeat[] = []; + let tagsDB: BeatTag[] = []; + + describe('remove_tags_from_beats', () => { + beforeEach(async () => { + beatsDB = [ + { + access_token: '9a6c99ae0fd84b068819701169cd8a4b', + host_ip: '1.2.3.4', + host_name: 'foo.bar.com', + id: 'qux', + type: 'filebeat', + }, + { + access_token: '188255eb560a4448b72656c5e99cae6f', + host_ip: '22.33.11.44', + host_name: 'baz.bar.com', + id: 'baz', + type: 'metricbeat', + }, + { + access_token: '93c4a4dd08564c189a7ec4e4f046b975', + host_ip: '1.2.3.4', + host_name: 'foo.bar.com', + id: 'foo', + tags: ['production', 'qa'], + type: 'metricbeat', + verified_on: '2018-05-15T16:25:38.924Z', + }, + { + access_token: '3c4a4dd08564c189a7ec4e4f046b9759', + host_ip: '11.22.33.44', + host_name: 'foo.com', + id: 'bar', + type: 'filebeat', + }, + ]; + tagsDB = [ + { + configuration_blocks: [], + id: 'production', + }, + { + configuration_blocks: [], + id: 'development', + }, + { + configuration_blocks: [], + id: 'qa', + }, + ]; + + libs = compose({ + tagsDB, + beatsDB, + }); + }); + + it('should remove a single tag from a single beat', async () => { + const apiResponse = await libs.beats.removeTagsFromBeats(internalUser, [ + { beatId: 'foo', tag: 'production' }, + ]); + + expect(apiResponse.removals).toEqual([{ status: 200, result: 'updated' }]); + // @ts-ignore + expect(beatsDB.find(b => b.id === 'foo').tags).toEqual(['qa']); + }); + + it('should remove a single tag from a multiple beats', async () => { + const apiResponse = await libs.beats.removeTagsFromBeats(internalUser, [ + { beatId: 'foo', tag: 'development' }, + { beatId: 'bar', tag: 'development' }, + ]); + + expect(apiResponse.removals).toEqual([ + { status: 200, result: 'updated' }, + { status: 200, result: 'updated' }, + ]); + + // @ts-ignore + expect(beatsDB.find(b => b.id === 'foo').tags).toEqual(['production', 'qa']); + expect(beatsDB.find(b => b.id === 'bar')).not.toHaveProperty('tags'); + }); + }); +}); diff --git a/x-pack/plugins/beats_management/server/lib/domains/__tests__/tokens.test.ts b/x-pack/plugins/beats_management/server/lib/domains/__tests__/tokens.test.ts index c89962dca7d3b..eca217b3c8ed1 100644 --- a/x-pack/plugins/beats_management/server/lib/domains/__tests__/tokens.test.ts +++ b/x-pack/plugins/beats_management/server/lib/domains/__tests__/tokens.test.ts @@ -28,7 +28,7 @@ describe('Token Domain Lib', () => { beforeEach(async () => { tokensDB = []; - framework = new TestingBackendFrameworkAdapter(null, settings); + framework = new TestingBackendFrameworkAdapter(settings); tokensLib = new CMTokensDomain(new MemoryTokensAdapter(tokensDB), { framework, diff --git a/x-pack/plugins/beats_management/server/lib/domains/beats.ts b/x-pack/plugins/beats_management/server/lib/domains/beats.ts index 618de8ab0b446..f3c98b056d5e1 100644 --- a/x-pack/plugins/beats_management/server/lib/domains/beats.ts +++ b/x-pack/plugins/beats_management/server/lib/domains/beats.ts @@ -14,28 +14,33 @@ import { FrameworkUser } from '../adapters/framework/adapter_types'; import { CMAssignmentReturn } from '../adapters/beats/adapter_types'; import { BeatsRemovalReturn } from '../adapters/beats/adapter_types'; -import { BeatEnrollmentStatus, CMDomainLibs } from '../lib'; +import { BeatEnrollmentStatus, CMDomainLibs, CMServerLibs } from '../lib'; export class CMBeatsDomain { - private adapter: CMBeatsAdapter; private tags: CMDomainLibs['tags']; private tokens: CMDomainLibs['tokens']; + private framework: CMServerLibs['framework']; constructor( - adapter: CMBeatsAdapter, - libs: { tags: CMDomainLibs['tags']; tokens: CMDomainLibs['tokens'] } + private readonly adapter: CMBeatsAdapter, + libs: { + tags: CMDomainLibs['tags']; + tokens: CMDomainLibs['tokens']; + framework: CMServerLibs['framework']; + } ) { this.adapter = adapter; this.tags = libs.tags; this.tokens = libs.tokens; + this.framework = libs.framework; } - public async getById(beatId: string) { - return await this.adapter.get(beatId); + public async getById(user: FrameworkUser, beatId: string) { + return await this.adapter.get(user, beatId); } public async update(beatId: string, accessToken: string, beatData: Partial) { - const beat = await this.adapter.get(beatId); + const beat = await this.adapter.get(this.framework.internalUser, beatId); const { verified: isAccessTokenValid } = this.tokens.verifyToken( beat ? beat.access_token : '', @@ -190,6 +195,7 @@ function addNonExistentItemToResponse( ) { assignments.forEach(({ beatId, tag }: BeatsTagAssignment, idx: any) => { const isBeatNonExistent = nonExistentBeatIds.includes(beatId); + const isTagNonExistent = nonExistentTags.includes(tag); if (isBeatNonExistent && isTagNonExistent) { diff --git a/x-pack/plugins/beats_management/server/lib/domains/tags.ts b/x-pack/plugins/beats_management/server/lib/domains/tags.ts index 36cf9a5e6d7f0..39bc2c147ea03 100644 --- a/x-pack/plugins/beats_management/server/lib/domains/tags.ts +++ b/x-pack/plugins/beats_management/server/lib/domains/tags.ts @@ -18,6 +18,10 @@ export class CMTagsDomain { this.adapter = adapter; } + public async getAll(user: FrameworkUser) { + return await this.adapter.getAll(user); + } + public async getTagsWithIds(user: FrameworkUser, tagIds: string[]) { return await this.adapter.getTagsWithIds(user, tagIds); } diff --git a/x-pack/plugins/beats_management/server/lib/lib.ts b/x-pack/plugins/beats_management/server/lib/lib.ts index 495093842b496..824c5722a4bd8 100644 --- a/x-pack/plugins/beats_management/server/lib/lib.ts +++ b/x-pack/plugins/beats_management/server/lib/lib.ts @@ -18,7 +18,7 @@ export interface CMDomainLibs { export interface CMServerLibs extends CMDomainLibs { framework: BackendFrameworkAdapter; - database: DatabaseAdapter; + database?: DatabaseAdapter; } export enum BeatEnrollmentStatus { diff --git a/x-pack/plugins/beats_management/server/management_server.ts b/x-pack/plugins/beats_management/server/management_server.ts index 1c8a1d727175e..f7316ea0887ff 100644 --- a/x-pack/plugins/beats_management/server/management_server.ts +++ b/x-pack/plugins/beats_management/server/management_server.ts @@ -16,10 +16,12 @@ import { createTokensRoute } from './rest_api/tokens/create'; import { beatsIndexTemplate } from './utils/index_templates'; export const initManagementServer = (libs: CMServerLibs) => { - libs.database.putTemplate(libs.framework.internalUser, { - name: 'beats-template', - body: beatsIndexTemplate, - }); + if (libs.database) { + libs.database.putTemplate(libs.framework.internalUser, { + name: 'beats-template', + body: beatsIndexTemplate, + }); + } libs.framework.registerRoute(createGetBeatConfigurationRoute(libs)); libs.framework.registerRoute(createTagAssignmentsRoute(libs)); diff --git a/x-pack/plugins/beats_management/server/rest_api/beats/configuration.ts b/x-pack/plugins/beats_management/server/rest_api/beats/configuration.ts index 81906ea9473e6..dad9909ddd1e0 100644 --- a/x-pack/plugins/beats_management/server/rest_api/beats/configuration.ts +++ b/x-pack/plugins/beats_management/server/rest_api/beats/configuration.ts @@ -27,7 +27,7 @@ export const createGetBeatConfigurationRoute = (libs: CMServerLibs) => ({ let beat; let tags; try { - beat = await libs.beats.getById(beatId); + beat = await libs.beats.getById(libs.framework.internalUser, beatId); if (beat === null) { return reply({ message: 'Beat not found' }).code(404); } diff --git a/x-pack/plugins/beats_management/server/rest_api/beats/enroll.ts b/x-pack/plugins/beats_management/server/rest_api/beats/enroll.ts index c0f0185fbde25..c1c23537218bd 100644 --- a/x-pack/plugins/beats_management/server/rest_api/beats/enroll.ts +++ b/x-pack/plugins/beats_management/server/rest_api/beats/enroll.ts @@ -13,6 +13,8 @@ import { wrapEsError } from '../../utils/error_wrappers'; // TODO: add license check pre-hook // TODO: write to Kibana audit log file export const createBeatEnrollmentRoute = (libs: CMServerLibs) => ({ + method: 'POST', + path: '/api/beats/agent/{beatId}', config: { auth: false, validate: { @@ -58,6 +60,4 @@ export const createBeatEnrollmentRoute = (libs: CMServerLibs) => ({ return reply(wrapEsError(err)); } }, - method: 'POST', - path: '/api/beats/agent/{beatId}', }); diff --git a/x-pack/plugins/beats_management/server/rest_api/beats/get.ts b/x-pack/plugins/beats_management/server/rest_api/beats/get.ts new file mode 100644 index 0000000000000..e7ed5ef5c2cc8 --- /dev/null +++ b/x-pack/plugins/beats_management/server/rest_api/beats/get.ts @@ -0,0 +1,39 @@ +/* + * 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 { CMBeat } from '../../../common/domain_types'; +import { CMServerLibs } from '../../lib/lib'; +import { wrapEsError } from '../../utils/error_wrappers'; + +export const createGetBeatRoute = (libs: CMServerLibs) => ({ + method: 'GET', + path: '/api/beats/agent/{beatId}', + config: { + validate: { + headers: Joi.object({ + 'kbn-beats-access-token': Joi.string().required(), + }).options({ allowUnknown: true }), + }, + }, + handler: async (request: any, reply: any) => { + const beatId = request.params.beatId; + + let beat: CMBeat; + try { + beat = await libs.beats.getById(request.user, beatId); + if (beat === null) { + return reply({ message: 'Beat not found' }).code(404); + } + } catch (err) { + return reply(wrapEsError(err)); + } + + delete beat.access_token; + + reply(beat); + }, +}); diff --git a/x-pack/plugins/beats_management/server/rest_api/beats/list.ts b/x-pack/plugins/beats_management/server/rest_api/beats/list.ts index a47bfcfbf3853..72105fc2f5440 100644 --- a/x-pack/plugins/beats_management/server/rest_api/beats/list.ts +++ b/x-pack/plugins/beats_management/server/rest_api/beats/list.ts @@ -10,6 +10,8 @@ import { wrapEsError } from '../../utils/error_wrappers'; // TODO: add license check pre-hook export const createListAgentsRoute = (libs: CMServerLibs) => ({ + method: 'GET', + path: '/api/beats/agents', handler: async (request: FrameworkRequest, reply: any) => { try { const beats = await libs.beats.getAllBeats(request.user); @@ -19,6 +21,4 @@ export const createListAgentsRoute = (libs: CMServerLibs) => ({ return reply(wrapEsError(err)); } }, - method: 'GET', - path: '/api/beats/agents', }); diff --git a/x-pack/plugins/beats_management/server/rest_api/beats/tag_assignment.ts b/x-pack/plugins/beats_management/server/rest_api/beats/tag_assignment.ts index 9c832ee3226b8..857b68a921597 100644 --- a/x-pack/plugins/beats_management/server/rest_api/beats/tag_assignment.ts +++ b/x-pack/plugins/beats_management/server/rest_api/beats/tag_assignment.ts @@ -5,19 +5,23 @@ */ import Joi from 'joi'; +import { BeatsTagAssignment } from '../../../public/lib/adapters/beats/adapter_types'; import { FrameworkRequest } from '../../lib/adapters/framework/adapter_types'; + import { CMServerLibs } from '../../lib/lib'; import { wrapEsError } from '../../utils/error_wrappers'; // TODO: add license check pre-hook // TODO: write to Kibana audit log file export const createTagAssignmentsRoute = (libs: CMServerLibs) => ({ + method: 'POST', + path: '/api/beats/agents_tags/assignments', config: { validate: { payload: Joi.object({ assignments: Joi.array().items( Joi.object({ - beat_id: Joi.string().required(), + beatId: Joi.string().required(), tag: Joi.string().required(), }) ), @@ -25,22 +29,14 @@ export const createTagAssignmentsRoute = (libs: CMServerLibs) => ({ }, }, handler: async (request: FrameworkRequest, reply: any) => { - const { assignments } = request.payload; - - // TODO abstract or change API to keep beatId consistent - const tweakedAssignments = assignments.map((assignment: any) => ({ - beatId: assignment.beat_id, - tag: assignment.tag, - })); + const { assignments }: { assignments: BeatsTagAssignment[] } = request.payload; try { - const response = await libs.beats.assignTagsToBeats(request.user, tweakedAssignments); + const response = await libs.beats.assignTagsToBeats(request.user, assignments); reply(response); } catch (err) { // TODO move this to kibana route thing in adapter return reply(wrapEsError(err)); } }, - method: 'POST', - path: '/api/beats/agents_tags/assignments', }); diff --git a/x-pack/plugins/beats_management/server/rest_api/beats/tag_removal.ts b/x-pack/plugins/beats_management/server/rest_api/beats/tag_removal.ts index 6a7af3b42d407..eaec8f2872172 100644 --- a/x-pack/plugins/beats_management/server/rest_api/beats/tag_removal.ts +++ b/x-pack/plugins/beats_management/server/rest_api/beats/tag_removal.ts @@ -12,6 +12,8 @@ import { wrapEsError } from '../../utils/error_wrappers'; // TODO: add license check pre-hook // TODO: write to Kibana audit log file export const createTagRemovalsRoute = (libs: CMServerLibs) => ({ + method: 'POST', + path: '/api/beats/agents_tags/removals', config: { validate: { payload: Joi.object({ @@ -41,6 +43,4 @@ export const createTagRemovalsRoute = (libs: CMServerLibs) => ({ return reply(wrapEsError(err)); } }, - method: 'POST', - path: '/api/beats/agents_tags/removals', }); diff --git a/x-pack/plugins/beats_management/server/rest_api/beats/update.ts b/x-pack/plugins/beats_management/server/rest_api/beats/update.ts index 60d7151731cd3..9c8e7daf475f8 100644 --- a/x-pack/plugins/beats_management/server/rest_api/beats/update.ts +++ b/x-pack/plugins/beats_management/server/rest_api/beats/update.ts @@ -12,6 +12,8 @@ import { wrapEsError } from '../../utils/error_wrappers'; // TODO: add license check pre-hook // TODO: write to Kibana audit log file (include who did the verification as well) export const createBeatUpdateRoute = (libs: CMServerLibs) => ({ + method: 'PUT', + path: '/api/beats/agent/{beatId}', config: { auth: false, validate: { @@ -56,6 +58,4 @@ export const createBeatUpdateRoute = (libs: CMServerLibs) => ({ return reply(wrapEsError(err)); } }, - method: 'PUT', - path: '/api/beats/agent/{beatId}', }); diff --git a/x-pack/plugins/beats_management/server/rest_api/tags/get.ts b/x-pack/plugins/beats_management/server/rest_api/tags/get.ts new file mode 100644 index 0000000000000..97aa6b413aeb3 --- /dev/null +++ b/x-pack/plugins/beats_management/server/rest_api/tags/get.ts @@ -0,0 +1,27 @@ +/* + * 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 { BeatTag } from '../../../common/domain_types'; +import { CMServerLibs } from '../../lib/lib'; +import { wrapEsError } from '../../utils/error_wrappers'; + +export const createGetTagsWithIdsRoute = (libs: CMServerLibs) => ({ + method: 'GET', + path: '/api/beats/tags/{tagIds}', + handler: async (request: any, reply: any) => { + const tagIdString: string = request.params.tagIds; + const tagIds = tagIdString.split(',').filter((id: string) => id.length > 0); + + let tags: BeatTag[]; + try { + tags = await libs.tags.getTagsWithIds(request.user, tagIds); + } catch (err) { + return reply(wrapEsError(err)); + } + + reply(tags); + }, +}); diff --git a/x-pack/plugins/beats_management/server/rest_api/tags/list.ts b/x-pack/plugins/beats_management/server/rest_api/tags/list.ts new file mode 100644 index 0000000000000..41874a77eef0f --- /dev/null +++ b/x-pack/plugins/beats_management/server/rest_api/tags/list.ts @@ -0,0 +1,24 @@ +/* + * 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 { BeatTag } from '../../../common/domain_types'; +import { CMServerLibs } from '../../lib/lib'; +import { wrapEsError } from '../../utils/error_wrappers'; + +export const createListTagsRoute = (libs: CMServerLibs) => ({ + method: 'GET', + path: '/api/beats/tags/', + handler: async (request: any, reply: any) => { + let tags: BeatTag[]; + try { + tags = await libs.tags.getAll(request.user); + } catch (err) { + return reply(wrapEsError(err)); + } + + reply(tags); + }, +}); diff --git a/x-pack/plugins/beats_management/server/rest_api/tags/set.ts b/x-pack/plugins/beats_management/server/rest_api/tags/set.ts index 6c01959e75311..f50721e764ef3 100644 --- a/x-pack/plugins/beats_management/server/rest_api/tags/set.ts +++ b/x-pack/plugins/beats_management/server/rest_api/tags/set.ts @@ -14,6 +14,8 @@ import { wrapEsError } from '../../utils/error_wrappers'; // TODO: add license check pre-hook // TODO: write to Kibana audit log file export const createSetTagRoute = (libs: CMServerLibs) => ({ + method: 'PUT', + path: '/api/beats/tag/{tag}', config: { validate: { params: Joi.object({ @@ -49,6 +51,4 @@ export const createSetTagRoute = (libs: CMServerLibs) => ({ return reply(wrapEsError(err)); } }, - method: 'PUT', - path: '/api/beats/tag/{tag}', }); diff --git a/x-pack/plugins/beats_management/server/rest_api/tokens/create.ts b/x-pack/plugins/beats_management/server/rest_api/tokens/create.ts index 74278703347c3..15ab7b872df8a 100644 --- a/x-pack/plugins/beats_management/server/rest_api/tokens/create.ts +++ b/x-pack/plugins/beats_management/server/rest_api/tokens/create.ts @@ -13,6 +13,8 @@ import { wrapEsError } from '../../utils/error_wrappers'; // TODO: write to Kibana audit log file const DEFAULT_NUM_TOKENS = 1; export const createTokensRoute = (libs: CMServerLibs) => ({ + method: 'POST', + path: '/api/beats/enrollment_tokens', config: { validate: { payload: Joi.object({ @@ -34,6 +36,4 @@ export const createTokensRoute = (libs: CMServerLibs) => ({ return reply(wrapEsError(err)); } }, - method: 'POST', - path: '/api/beats/enrollment_tokens', }); diff --git a/x-pack/test/api_integration/apis/beats/assign_tags_to_beats.js b/x-pack/test/api_integration/apis/beats/assign_tags_to_beats.js index 88b7b7c3feb3a..621a6187693bd 100644 --- a/x-pack/test/api_integration/apis/beats/assign_tags_to_beats.js +++ b/x-pack/test/api_integration/apis/beats/assign_tags_to_beats.js @@ -5,10 +5,7 @@ */ import expect from 'expect.js'; -import { - ES_INDEX_NAME, - ES_TYPE_NAME -} from './constants'; +import { ES_INDEX_NAME, ES_TYPE_NAME } from './constants'; export default function ({ getService }) { const supertest = getService('supertest'); @@ -24,25 +21,19 @@ export default function ({ getService }) { it('should add a single tag to a single beat', async () => { const { body: apiResponse } = await supertest - .post( - '/api/beats/agents_tags/assignments' - ) + .post('/api/beats/agents_tags/assignments') .set('kbn-xsrf', 'xxx') .send({ - assignments: [ - { beat_id: 'bar', tag: 'production' } - ] + assignments: [{ beatId: 'bar', tag: 'production' }], }) .expect(200); - expect(apiResponse.assignments).to.eql([ - { status: 200, result: 'updated' } - ]); + expect(apiResponse.assignments).to.eql([{ status: 200, result: 'updated' }]); const esResponse = await es.get({ index: ES_INDEX_NAME, type: ES_TYPE_NAME, - id: `beat:bar` + id: `beat:bar`, }); const beat = esResponse._source.beat; @@ -59,7 +50,7 @@ export default function ({ getService }) { esResponse = await es.get({ index: ES_INDEX_NAME, type: ES_TYPE_NAME, - id: `beat:foo` + id: `beat:foo`, }); beat = esResponse._source.beat; @@ -67,26 +58,20 @@ export default function ({ getService }) { // Adding the existing tag const { body: apiResponse } = await supertest - .post( - '/api/beats/agents_tags/assignments' - ) + .post('/api/beats/agents_tags/assignments') .set('kbn-xsrf', 'xxx') .send({ - assignments: [ - { beat_id: 'foo', tag: 'production' } - ] + assignments: [{ beatId: 'foo', tag: 'production' }], }) .expect(200); - expect(apiResponse.assignments).to.eql([ - { status: 200, result: 'updated' } - ]); + expect(apiResponse.assignments).to.eql([{ status: 200, result: 'updated' }]); // After adding the existing tag esResponse = await es.get({ index: ES_INDEX_NAME, type: ES_TYPE_NAME, - id: `beat:foo` + id: `beat:foo`, }); beat = esResponse._source.beat; @@ -95,21 +80,19 @@ export default function ({ getService }) { it('should add a single tag to a multiple beats', async () => { const { body: apiResponse } = await supertest - .post( - '/api/beats/agents_tags/assignments' - ) + .post('/api/beats/agents_tags/assignments') .set('kbn-xsrf', 'xxx') .send({ assignments: [ - { beat_id: 'foo', tag: 'development' }, - { beat_id: 'bar', tag: 'development' } - ] + { beatId: 'foo', tag: 'development' }, + { beatId: 'bar', tag: 'development' }, + ], }) .expect(200); expect(apiResponse.assignments).to.eql([ { status: 200, result: 'updated' }, - { status: 200, result: 'updated' } + { status: 200, result: 'updated' }, ]); let esResponse; @@ -119,7 +102,7 @@ export default function ({ getService }) { esResponse = await es.get({ index: ES_INDEX_NAME, type: ES_TYPE_NAME, - id: `beat:foo` + id: `beat:foo`, }); beat = esResponse._source.beat; @@ -129,7 +112,7 @@ export default function ({ getService }) { esResponse = await es.get({ index: ES_INDEX_NAME, type: ES_TYPE_NAME, - id: `beat:bar` + id: `beat:bar`, }); beat = esResponse._source.beat; @@ -138,27 +121,25 @@ export default function ({ getService }) { it('should add multiple tags to a single beat', async () => { const { body: apiResponse } = await supertest - .post( - '/api/beats/agents_tags/assignments' - ) + .post('/api/beats/agents_tags/assignments') .set('kbn-xsrf', 'xxx') .send({ assignments: [ - { beat_id: 'bar', tag: 'development' }, - { beat_id: 'bar', tag: 'production' } - ] + { beatId: 'bar', tag: 'development' }, + { beatId: 'bar', tag: 'production' }, + ], }) .expect(200); expect(apiResponse.assignments).to.eql([ { status: 200, result: 'updated' }, - { status: 200, result: 'updated' } + { status: 200, result: 'updated' }, ]); const esResponse = await es.get({ index: ES_INDEX_NAME, type: ES_TYPE_NAME, - id: `beat:bar` + id: `beat:bar`, }); const beat = esResponse._source.beat; @@ -167,21 +148,19 @@ export default function ({ getService }) { it('should add multiple tags to a multiple beats', async () => { const { body: apiResponse } = await supertest - .post( - '/api/beats/agents_tags/assignments' - ) + .post('/api/beats/agents_tags/assignments') .set('kbn-xsrf', 'xxx') .send({ assignments: [ - { beat_id: 'foo', tag: 'development' }, - { beat_id: 'bar', tag: 'production' } - ] + { beatId: 'foo', tag: 'development' }, + { beatId: 'bar', tag: 'production' }, + ], }) .expect(200); expect(apiResponse.assignments).to.eql([ { status: 200, result: 'updated' }, - { status: 200, result: 'updated' } + { status: 200, result: 'updated' }, ]); let esResponse; @@ -191,7 +170,7 @@ export default function ({ getService }) { esResponse = await es.get({ index: ES_INDEX_NAME, type: ES_TYPE_NAME, - id: `beat:foo` + id: `beat:foo`, }); beat = esResponse._source.beat; @@ -201,7 +180,7 @@ export default function ({ getService }) { esResponse = await es.get({ index: ES_INDEX_NAME, type: ES_TYPE_NAME, - id: `beat:bar` + id: `beat:bar`, }); beat = esResponse._source.beat; @@ -212,19 +191,15 @@ export default function ({ getService }) { const nonExistentBeatId = chance.word(); const { body: apiResponse } = await supertest - .post( - '/api/beats/agents_tags/assignments' - ) + .post('/api/beats/agents_tags/assignments') .set('kbn-xsrf', 'xxx') .send({ - assignments: [ - { beat_id: nonExistentBeatId, tag: 'production' } - ] + assignments: [{ beatId: nonExistentBeatId, tag: 'production' }], }) .expect(200); expect(apiResponse.assignments).to.eql([ - { status: 404, result: `Beat ${nonExistentBeatId} not found` } + { status: 404, result: `Beat ${nonExistentBeatId} not found` }, ]); }); @@ -232,25 +207,21 @@ export default function ({ getService }) { const nonExistentTag = chance.word(); const { body: apiResponse } = await supertest - .post( - '/api/beats/agents_tags/assignments' - ) + .post('/api/beats/agents_tags/assignments') .set('kbn-xsrf', 'xxx') .send({ - assignments: [ - { beat_id: 'bar', tag: nonExistentTag } - ] + assignments: [{ beatID: 'bar', tag: nonExistentTag }], }) .expect(200); expect(apiResponse.assignments).to.eql([ - { status: 404, result: `Tag ${nonExistentTag} not found` } + { status: 404, result: `Tag ${nonExistentTag} not found` }, ]); const esResponse = await es.get({ index: ES_INDEX_NAME, type: ES_TYPE_NAME, - id: `beat:bar` + id: `beat:bar`, }); const beat = esResponse._source.beat; @@ -262,25 +233,21 @@ export default function ({ getService }) { const nonExistentTag = chance.word(); const { body: apiResponse } = await supertest - .post( - '/api/beats/agents_tags/assignments' - ) + .post('/api/beats/agents_tags/assignments') .set('kbn-xsrf', 'xxx') .send({ - assignments: [ - { beat_id: nonExistentBeatId, tag: nonExistentTag } - ] + assignments: [{ beatID: nonExistentBeatId, tag: nonExistentTag }], }) .expect(200); expect(apiResponse.assignments).to.eql([ - { status: 404, result: `Beat ${nonExistentBeatId} and tag ${nonExistentTag} not found` } + { status: 404, result: `Beat ${nonExistentBeatId} and tag ${nonExistentTag} not found` }, ]); const esResponse = await es.get({ index: ES_INDEX_NAME, type: ES_TYPE_NAME, - id: `beat:bar` + id: `beat:bar`, }); const beat = esResponse._source.beat; From 1369e68b2540a4d9e2b3d23cca68d81de6113cba Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Fri, 3 Aug 2018 10:28:55 -0400 Subject: [PATCH 28/43] [Beats Management] Add BeatsTable/Bulk Action Search Component (#21182) * Add BeatsTable and control bar components. * Clean yarn.lock. * Move raw numbers/strings to constants. Remove obsolete state/props. * Update/add tests. * Change prop name from "items" to "beats". * Rename some variables. * Move search bar filter definitions to table render. * Update table to support assignment options. * Update action control position. * Refactor split render function into custom components. --- .../common/constants/index.ts | 1 + .../common/constants/table.ts | 10 ++ .../beats_management/common/domain_types.ts | 7 ++ .../public/components/table/action_button.tsx | 63 ++++++++++ .../components/table/assignment_options.tsx | 103 ++++++++++++++++ .../public/components/table/controls.tsx | 48 ++++++++ .../public/components/table/index.ts | 9 ++ .../public/components/table/table.tsx | 88 +++++++++++++ .../components/table/table_type_configs.tsx | 116 ++++++++++++++++++ .../public/pages/main/beats.tsx | 105 +++++++++++++++- .../lib/adapters/beats/adapter_types.ts | 7 +- x-pack/plugins/beats_management/wallaby.js | 3 +- 12 files changed, 552 insertions(+), 8 deletions(-) create mode 100644 x-pack/plugins/beats_management/common/constants/table.ts create mode 100644 x-pack/plugins/beats_management/public/components/table/action_button.tsx create mode 100644 x-pack/plugins/beats_management/public/components/table/assignment_options.tsx create mode 100644 x-pack/plugins/beats_management/public/components/table/controls.tsx create mode 100644 x-pack/plugins/beats_management/public/components/table/index.ts create mode 100644 x-pack/plugins/beats_management/public/components/table/table.tsx create mode 100644 x-pack/plugins/beats_management/public/components/table/table_type_configs.tsx diff --git a/x-pack/plugins/beats_management/common/constants/index.ts b/x-pack/plugins/beats_management/common/constants/index.ts index b4e919607c604..50851dcef947e 100644 --- a/x-pack/plugins/beats_management/common/constants/index.ts +++ b/x-pack/plugins/beats_management/common/constants/index.ts @@ -8,3 +8,4 @@ export { PLUGIN } from './plugin'; export { INDEX_NAMES } from './index_names'; export { UNIQUENESS_ENFORCING_TYPES, ConfigurationBlockTypes } from './configuration_blocks'; export const BASE_PATH = '/management/beats_management/'; +export { TABLE_CONFIG } from './table'; diff --git a/x-pack/plugins/beats_management/common/constants/table.ts b/x-pack/plugins/beats_management/common/constants/table.ts new file mode 100644 index 0000000000000..801a60082d1b8 --- /dev/null +++ b/x-pack/plugins/beats_management/common/constants/table.ts @@ -0,0 +1,10 @@ +/* + * 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 TABLE_CONFIG = { + INITIAL_ROW_SIZE: 5, + PAGE_SIZE_OPTIONS: [3, 5, 10, 20], +}; diff --git a/x-pack/plugins/beats_management/common/domain_types.ts b/x-pack/plugins/beats_management/common/domain_types.ts index 9411aca413840..a553f987a233e 100644 --- a/x-pack/plugins/beats_management/common/domain_types.ts +++ b/x-pack/plugins/beats_management/common/domain_types.ts @@ -19,13 +19,20 @@ export interface CMBeat { host_ip: string; host_name: string; ephemeral_id?: string; + last_updated?: string; + event_rate?: string; local_configuration_yml?: string; tags?: string[]; central_configuration_yml?: string; metadata?: {}; } +export interface CMPopulatedBeat extends CMBeat { + full_tags: BeatTag[]; +} + export interface BeatTag { id: string; configuration_blocks: ConfigurationBlock[]; + color?: string; } diff --git a/x-pack/plugins/beats_management/public/components/table/action_button.tsx b/x-pack/plugins/beats_management/public/components/table/action_button.tsx new file mode 100644 index 0000000000000..e91e620ed8d8c --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/table/action_button.tsx @@ -0,0 +1,63 @@ +/* + * 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 { EuiButton, EuiContextMenu, EuiPopover } from '@elastic/eui'; +import React from 'react'; +import { ActionDefinition } from './table_type_configs'; + +interface ActionButtonProps { + actions: ActionDefinition[]; + isPopoverVisible: boolean; + actionHandler(action: string, payload?: any): void; + hidePopover(): void; + showPopover(): void; +} + +export function ActionButton(props: ActionButtonProps) { + const { actions, actionHandler, hidePopover, isPopoverVisible, showPopover } = props; + if (actions.length === 0) { + return null; + } else if (actions.length === 1) { + const action = actions[0]; + return ( + actionHandler(action.action)} + > + {action.name} + + ); + } + return ( + + Bulk Action + + } + closePopover={hidePopover} + id="contextMenu" + isOpen={isPopoverVisible} + panelPaddingSize="none" + withTitle + > + ({ + ...action, + onClick: () => actionHandler(action.action), + })), + }, + ]} + /> + + ); +} diff --git a/x-pack/plugins/beats_management/public/components/table/assignment_options.tsx b/x-pack/plugins/beats_management/public/components/table/assignment_options.tsx new file mode 100644 index 0000000000000..60a6bf2b46952 --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/table/assignment_options.tsx @@ -0,0 +1,103 @@ +/* + * 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 { EuiButton, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiPopover } from '@elastic/eui'; +import React from 'react'; +import { ActionButton } from './action_button'; +import { ControlDefinitions } from './table_type_configs'; + +interface AssignmentOptionsProps { + assignmentOptions: any[] | null; + assignmentTitle: string | null; + controlDefinitions: ControlDefinitions; + selectionCount: number; + actionHandler(action: string, payload?: any): void; +} + +interface AssignmentOptionsState { + isAssignmentPopoverVisible: boolean; + isActionPopoverVisible: boolean; +} + +export class AssignmentOptions extends React.Component< + AssignmentOptionsProps, + AssignmentOptionsState +> { + constructor(props: AssignmentOptionsProps) { + super(props); + + this.state = { + isAssignmentPopoverVisible: false, + isActionPopoverVisible: false, + }; + } + + public render() { + const { + actionHandler, + assignmentOptions, + assignmentTitle, + controlDefinitions: { actions }, + selectionCount, + } = this.props; + const { isActionPopoverVisible, isAssignmentPopoverVisible } = this.state; + return ( + + {selectionCount} selected + + { + this.setState({ isActionPopoverVisible: false }); + }} + isPopoverVisible={isActionPopoverVisible} + showPopover={() => { + this.setState({ isActionPopoverVisible: true }); + }} + /> + + + { + this.setState({ + isAssignmentPopoverVisible: true, + }); + actionHandler('loadAssignmentOptions'); + }} + > + {assignmentTitle} + + } + closePopover={() => { + this.setState({ isAssignmentPopoverVisible: false }); + }} + id="assignmentList" + isOpen={isAssignmentPopoverVisible} + panelPaddingSize="s" + withTitle + > + {assignmentOptions ? ( + // @ts-ignore direction prop not available on current typing + + {assignmentOptions} + + ) : ( +
+ Loading +
+ )} +
+
+
+ ); + } +} diff --git a/x-pack/plugins/beats_management/public/components/table/controls.tsx b/x-pack/plugins/beats_management/public/components/table/controls.tsx new file mode 100644 index 0000000000000..5182c10c71722 --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/table/controls.tsx @@ -0,0 +1,48 @@ +/* + * 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 { + // @ts-ignore typings for EuiSearchar not included in EUI + EuiSearchBar, +} from '@elastic/eui'; +import React from 'react'; +import { AssignmentOptions } from './assignment_options'; +import { ControlDefinitions } from './table_type_configs'; + +interface ControlBarProps { + assignmentOptions: any[] | null; + assignmentTitle: string | null; + showAssignmentOptions: boolean; + controlDefinitions: ControlDefinitions; + selectionCount: number; + actionHandler(actionType: string, payload?: any): void; +} + +export function ControlBar(props: ControlBarProps) { + const { + actionHandler, + assignmentOptions, + assignmentTitle, + controlDefinitions, + selectionCount, + showAssignmentOptions, + } = props; + return selectionCount !== 0 && showAssignmentOptions ? ( + + ) : ( + actionHandler('search', query)} + /> + ); +} diff --git a/x-pack/plugins/beats_management/public/components/table/index.ts b/x-pack/plugins/beats_management/public/components/table/index.ts new file mode 100644 index 0000000000000..0789cad5c3022 --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/table/index.ts @@ -0,0 +1,9 @@ +/* + * 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 { Table } from './table'; +export { ControlBar } from './controls'; +export { BeatsTableType } from './table_type_configs'; diff --git a/x-pack/plugins/beats_management/public/components/table/table.tsx b/x-pack/plugins/beats_management/public/components/table/table.tsx new file mode 100644 index 0000000000000..c66c290b2f5f9 --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/table/table.tsx @@ -0,0 +1,88 @@ +/* + * 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 { + // @ts-ignore no typings for EuiInMemoryTable in EUI + EuiInMemoryTable, + EuiSpacer, +} from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; +import { TABLE_CONFIG } from '../../../common/constants'; +import { CMPopulatedBeat } from '../../../common/domain_types'; +import { ControlBar } from './controls'; +import { TableType } from './table_type_configs'; + +interface BeatsTableProps { + assignmentOptions: any[] | null; + assignmentTitle: string | null; + items: any[]; + type: TableType; + actionHandler(action: string, payload?: any): void; +} + +interface BeatsTableState { + selection: CMPopulatedBeat[]; +} + +const TableContainer = styled.div` + padding: 16px; +`; + +export class Table extends React.Component { + constructor(props: BeatsTableProps) { + super(props); + + this.state = { + selection: [], + }; + } + + public render() { + const { actionHandler, assignmentOptions, assignmentTitle, items, type } = this.props; + + const pagination = { + initialPageSize: TABLE_CONFIG.INITIAL_ROW_SIZE, + pageSizeOptions: TABLE_CONFIG.PAGE_SIZE_OPTIONS, + }; + + const selectionOptions = { + onSelectionChange: this.setSelection, + selectable: () => true, + selectableMessage: () => 'Select this beat', + selection: this.state.selection, + }; + + return ( + + actionHandler(action, payload)} + assignmentOptions={assignmentOptions} + assignmentTitle={assignmentTitle} + controlDefinitions={type.controlDefinitions(items)} + selectionCount={this.state.selection.length} + showAssignmentOptions={true} + /> + + + + ); + } + + private setSelection = (selection: any) => { + this.setState({ + selection, + }); + }; +} diff --git a/x-pack/plugins/beats_management/public/components/table/table_type_configs.tsx b/x-pack/plugins/beats_management/public/components/table/table_type_configs.tsx new file mode 100644 index 0000000000000..fb4529da171cb --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/table/table_type_configs.tsx @@ -0,0 +1,116 @@ +/* + * 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. + */ + +// @ts-ignore +import { EuiBadge, EuiFlexGroup, EuiIcon, EuiLink } from '@elastic/eui'; +import { flatten, uniq } from 'lodash'; +import moment from 'moment'; +import React from 'react'; +import { CMPopulatedBeat } from '../../../common/domain_types'; + +export interface ColumnDefinition { + field: string; + name: string; + sortable?: boolean; + render?(value: any, object?: any): any; +} + +export interface ActionDefinition { + action: string; + danger?: boolean; + icon?: any; + name: string; +} + +interface FilterOption { + value: string; +} + +export interface FilterDefinition { + field: string; + name: string; + options?: FilterOption[]; + type: string; +} + +export interface ControlDefinitions { + actions: ActionDefinition[]; + filters: FilterDefinition[]; +} + +export interface TableType { + columnDefinitions: ColumnDefinition[]; + controlDefinitions(items: any[]): ControlDefinitions; +} + +export const BeatsTableType: TableType = { + columnDefinitions: [ + { + field: 'id', + name: 'Beat name', + render: (id: string) => {id}, + sortable: true, + }, + { + field: 'type', + name: 'Type', + sortable: true, + }, + { + field: 'full_tags', + name: 'Tags', + render: (value: string, beat: CMPopulatedBeat) => ( + + {beat.full_tags.map(tag => ( + + {tag.id} + + ))} + + ), + sortable: false, + }, + { + // TODO: update to use actual metadata field + field: 'event_rate', + name: 'Event rate', + sortable: true, + }, + { + // TODO: update to use actual metadata field + field: 'last_updated', + name: 'Last config update', + render: (value: Date) =>
{moment(value).fromNow()}
, + sortable: true, + }, + ], + controlDefinitions: (data: any) => ({ + actions: [ + { + name: 'Disenroll Selected', + action: 'delete', + danger: true, + }, + ], + filters: [ + { + type: 'field_value_selection', + field: 'type', + name: 'Type', + options: uniq(data.map(({ type }: { type: any }) => ({ value: type })), 'value'), + }, + { + type: 'field_value_selection', + field: 'full_tags', + name: 'Tags', + options: uniq( + flatten(data.map((item: any) => item.full_tags)).map((tag: any) => ({ value: tag.id })), + 'value' + ), + }, + ], + }), +}; diff --git a/x-pack/plugins/beats_management/public/pages/main/beats.tsx b/x-pack/plugins/beats_management/public/pages/main/beats.tsx index a8c7518a3e27c..1d8a9e6717605 100644 --- a/x-pack/plugins/beats_management/public/pages/main/beats.tsx +++ b/x-pack/plugins/beats_management/public/pages/main/beats.tsx @@ -5,8 +5,11 @@ */ import { + // @ts-ignore typings for EuiBadge not present in current version + EuiBadge, EuiButton, EuiButtonEmpty, + EuiFlexItem, EuiModal, EuiModalBody, EuiModalFooter, @@ -15,16 +18,20 @@ import { EuiOverlayMask, } from '@elastic/eui'; -import { CMBeat } from '../../../common/domain_types'; +import React from 'react'; +import { BeatTag, CMBeat, CMPopulatedBeat } from '../../../common/domain_types'; +import { BeatsTagAssignment } from '../../../server/lib/adapters/beats/adapter_types'; +import { BeatsTableType, Table } from '../../components/table'; import { FrontendLibs } from '../../lib/lib'; -import React from 'react'; interface BeatsPageProps { libs: FrontendLibs; } interface BeatsPageState { beats: CMBeat[]; + tags: any[] | null; + tableRef: any; } export class BeatsPage extends React.PureComponent { @@ -72,17 +79,109 @@ export class BeatsPage extends React.PureComponentbeats table and stuff - {this.state.beats.length}; + return ( + + ); } + + private handleBeatsActions = (action: string, payload: any) => { + switch (action) { + case 'edit': + // TODO: navigate to edit page + break; + case 'delete': + this.deleteSelected(); + break; + case 'search': + this.handleSearchQuery(payload); + break; + case 'loadAssignmentOptions': + this.loadTags(); + break; + } + + this.loadBeats(); + }; + + // TODO: call delete endpoint + private deleteSelected = async () => { + // const selected = this.getSelectedBeats(); + // await this.props.libs.beats.delete(selected); + }; + private async loadBeats() { const beats = await this.props.libs.beats.getAll(); this.setState({ beats, }); } + + // todo: add reference to ES filter endpoint + private handleSearchQuery = (query: any) => { + // await this.props.libs.beats.searach(query); + }; + + private loadTags = async () => { + const tags = await this.props.libs.tags.getAll(); + const selectedBeats = this.getSelectedBeats(); + + const renderedTags = tags.map((tag: BeatTag) => { + const hasMatches = selectedBeats.some((beat: any) => + beat.full_tags.some((t: any) => t.id === tag.id) + ); + + return ( + + this.removeTagsFromBeats(selectedBeats, tag) + : () => this.assignTagsToBeats(selectedBeats, tag) + } + onClickAriaLabel={tag.id} + > + {tag.id} + + + ); + }); + this.setState({ + tags: renderedTags, + }); + }; + + private createBeatTagAssignments = ( + beats: CMPopulatedBeat[], + tag: BeatTag + ): BeatsTagAssignment[] => beats.map(({ id }) => ({ beatId: id, tag: tag.id })); + + private removeTagsFromBeats = async (beats: CMPopulatedBeat[], tag: BeatTag) => { + await this.props.libs.beats.removeTagsFromBeats(this.createBeatTagAssignments(beats, tag)); + this.loadBeats(); + }; + + private assignTagsToBeats = async (beats: CMPopulatedBeat[], tag: BeatTag) => { + await this.props.libs.beats.assignTagsToBeats(this.createBeatTagAssignments(beats, tag)); + this.loadBeats(); + }; + + private getSelectedBeats = () => { + return this.state.tableRef.current.state.selection; + }; } diff --git a/x-pack/plugins/beats_management/server/lib/adapters/beats/adapter_types.ts b/x-pack/plugins/beats_management/server/lib/adapters/beats/adapter_types.ts index 0d68fd528d42a..1f1f2fd13e3bc 100644 --- a/x-pack/plugins/beats_management/server/lib/adapters/beats/adapter_types.ts +++ b/x-pack/plugins/beats_management/server/lib/adapters/beats/adapter_types.ts @@ -7,13 +7,12 @@ import { CMBeat } from '../../../../common/domain_types'; import { FrameworkUser } from '../framework/adapter_types'; -// FIXME: fix getBeatsWithIds return type export interface CMBeatsAdapter { insert(beat: CMBeat): Promise; update(beat: CMBeat): Promise; - get(user: FrameworkUser, id: string): any; - getAll(user: FrameworkUser): any; - getWithIds(user: FrameworkUser, beatIds: string[]): any; + get(user: FrameworkUser, id: string): Promise; + getAll(user: FrameworkUser): Promise; + getWithIds(user: FrameworkUser, beatIds: string[]): Promise; removeTagsFromBeats( user: FrameworkUser, removals: BeatsTagAssignment[] diff --git a/x-pack/plugins/beats_management/wallaby.js b/x-pack/plugins/beats_management/wallaby.js index 8c0c4aa355925..e9901ff5bf23e 100644 --- a/x-pack/plugins/beats_management/wallaby.js +++ b/x-pack/plugins/beats_management/wallaby.js @@ -14,10 +14,11 @@ module.exports = function (wallaby) { //'plugins/beats/public/**/*.+(js|jsx|ts|tsx|json|snap|css|less|sass|scss|jpg|jpeg|gif|png|svg)', 'server/**/*.+(js|jsx|ts|tsx|json|snap|css|less|sass|scss|jpg|jpeg|gif|png|svg)', 'common/**/*.+(js|jsx|ts|tsx|json|snap|css|less|sass|scss|jpg|jpeg|gif|png|svg)', + 'public/**/*.+(js|jsx|ts|tsx|json|snap|css|less|sass|scss|jpg|jpeg|gif|png|svg)', '!**/*.test.ts', ], - tests: ['**/*.test.ts'], + tests: ['**/*.test.ts', '**/*.test.tsx'], env: { type: 'node', runner: 'node', From 93dac60f429b975e553742cad4022da482078b68 Mon Sep 17 00:00:00 2001 From: Matt Apperson Date: Fri, 3 Aug 2018 17:58:03 -0400 Subject: [PATCH 29/43] Beats/basic use cases (#21660) * tweak adapter responses / types. re-add enroll ui * routes enabled, enroll now pings the server * full enrollment path now working * improved pinging for beat enrollment * fix location of history call * reload beats list on beat enrollment completion --- .../beats_management/common/domain_types.ts | 1 + .../components/table/table_type_configs.tsx | 8 +- .../plugins/beats_management/public/index.tsx | 2 +- .../lib/adapters/beats/adapter_types.ts | 1 + .../adapters/beats/memory_beats_adapter.ts | 4 +- .../lib/adapters/beats/rest_beats_adapter.ts | 25 ++- .../lib/adapters/tags/rest_tags_adapter.ts | 8 +- .../lib/adapters/tokens/adapter_types.ts | 6 +- .../adapters/tokens/memory_tokens_adapter.ts | 9 +- .../adapters/tokens/rest_tokens_adapter.ts | 7 +- .../public/pages/main/beats.tsx | 57 +----- .../public/pages/main/beats_action_area.tsx | 162 ++++++++++++++++++ .../public/pages/main/index.tsx | 17 +- x-pack/plugins/beats_management/readme.md | 9 + .../beats_management/scripts/enroll.js | 34 ++++ .../lib/adapters/beats/adapter_types.ts | 7 +- .../beats/elasticsearch_beats_adapter.ts | 27 ++- .../adapters/beats/memory_beats_adapter.ts | 9 +- .../server/lib/domains/beats.ts | 5 + .../server/management_server.ts | 6 + .../server/rest_api/beats/get.ts | 36 ++-- .../utils/index_templates/beats_template.json | 8 +- .../api_integration/apis/beats/enroll_beat.js | 2 +- 23 files changed, 336 insertions(+), 114 deletions(-) create mode 100644 x-pack/plugins/beats_management/public/pages/main/beats_action_area.tsx create mode 100644 x-pack/plugins/beats_management/scripts/enroll.js diff --git a/x-pack/plugins/beats_management/common/domain_types.ts b/x-pack/plugins/beats_management/common/domain_types.ts index a553f987a233e..b943cc2cedc2b 100644 --- a/x-pack/plugins/beats_management/common/domain_types.ts +++ b/x-pack/plugins/beats_management/common/domain_types.ts @@ -12,6 +12,7 @@ export interface ConfigurationBlock { export interface CMBeat { id: string; + enrollment_token: string; access_token: string; verified_on?: string; type: string; diff --git a/x-pack/plugins/beats_management/public/components/table/table_type_configs.tsx b/x-pack/plugins/beats_management/public/components/table/table_type_configs.tsx index fb4529da171cb..d167d9aac3e22 100644 --- a/x-pack/plugins/beats_management/public/components/table/table_type_configs.tsx +++ b/x-pack/plugins/beats_management/public/components/table/table_type_configs.tsx @@ -64,7 +64,7 @@ export const BeatsTableType: TableType = { name: 'Tags', render: (value: string, beat: CMPopulatedBeat) => ( - {beat.full_tags.map(tag => ( + {(beat.full_tags || []).map(tag => ( {tag.id} @@ -87,7 +87,7 @@ export const BeatsTableType: TableType = { sortable: true, }, ], - controlDefinitions: (data: any) => ({ + controlDefinitions: (data: any[]) => ({ actions: [ { name: 'Disenroll Selected', @@ -107,7 +107,9 @@ export const BeatsTableType: TableType = { field: 'full_tags', name: 'Tags', options: uniq( - flatten(data.map((item: any) => item.full_tags)).map((tag: any) => ({ value: tag.id })), + flatten(data.map((item: any) => item.full_tags || [])).map((tag: any) => ({ + value: tag.id, + })), 'value' ), }, diff --git a/x-pack/plugins/beats_management/public/index.tsx b/x-pack/plugins/beats_management/public/index.tsx index 670aa23f8695b..9da5a99fc7028 100644 --- a/x-pack/plugins/beats_management/public/index.tsx +++ b/x-pack/plugins/beats_management/public/index.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { BASE_PATH } from '../common/constants'; -import { compose } from './lib/compose/memory'; +import { compose } from './lib/compose/kibana'; import { FrontendLibs } from './lib/lib'; // import * as euiVars from '@elastic/eui/dist/eui_theme_k6_light.json'; diff --git a/x-pack/plugins/beats_management/public/lib/adapters/beats/adapter_types.ts b/x-pack/plugins/beats_management/public/lib/adapters/beats/adapter_types.ts index b703edb971299..38129f27ab38f 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/beats/adapter_types.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/beats/adapter_types.ts @@ -11,6 +11,7 @@ export interface CMBeatsAdapter { getAll(): Promise; removeTagsFromBeats(removals: BeatsTagAssignment[]): Promise; assignTagsToBeats(assignments: BeatsTagAssignment[]): Promise; + getBeatWithToken(enrollmentToken: string): Promise; } export interface BeatsTagAssignment { diff --git a/x-pack/plugins/beats_management/public/lib/adapters/beats/memory_beats_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/beats/memory_beats_adapter.ts index 1940ec2ada4b0..565ed0932214f 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/beats/memory_beats_adapter.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/beats/memory_beats_adapter.ts @@ -28,7 +28,9 @@ export class MemoryBeatsAdapter implements CMBeatsAdapter { public async getAll() { return this.beatsDB.map((beat: any) => omit(beat, ['access_token'])); } - + public async getBeatWithToken(enrollmentToken: string): Promise { + return this.beatsDB.map((beat: any) => omit(beat, ['access_token']))[0]; + } public async removeTagsFromBeats(removals: BeatsTagAssignment[]): Promise { const beatIds = removals.map(r => r.beatId); diff --git a/x-pack/plugins/beats_management/public/lib/adapters/beats/rest_beats_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/beats/rest_beats_adapter.ts index 9da760be307db..aaf9d545dffee 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/beats/rest_beats_adapter.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/beats/rest_beats_adapter.ts @@ -19,19 +19,30 @@ export class RestBeatsAdapter implements CMBeatsAdapter { return await this.REST.get(`/api/beats/agent/${id}`); } + public async getBeatWithToken(enrollmentToken: string): Promise { + const beat = await this.REST.get(`/api/beats/agent/unknown/${enrollmentToken}`); + return beat; + } + public async getAll(): Promise { - return await this.REST.get('/api/beats/agents'); + return (await this.REST.get<{ beats: CMBeat[] }>('/api/beats/agents')).beats; } public async removeTagsFromBeats(removals: BeatsTagAssignment[]): Promise { - return await this.REST.post(`/api/beats/agents_tags/removals`, { - removals, - }); + return (await this.REST.post<{ removals: BeatsRemovalReturn[] }>( + `/api/beats/agents_tags/removals`, + { + removals, + } + )).removals; } public async assignTagsToBeats(assignments: BeatsTagAssignment[]): Promise { - return await this.REST.post(`/api/beats/agents_tags/assignments`, { - assignments, - }); + return (await this.REST.post<{ assignments: CMAssignmentReturn[] }>( + `/api/beats/agents_tags/assignments`, + { + assignments, + } + )).assignments; } } diff --git a/x-pack/plugins/beats_management/public/lib/adapters/tags/rest_tags_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/tags/rest_tags_adapter.ts index 5eb16762ba825..fc4a5a157ed71 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/tags/rest_tags_adapter.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/tags/rest_tags_adapter.ts @@ -12,16 +12,16 @@ export class RestTagsAdapter implements CMTagsAdapter { constructor(private readonly REST: RestAPIAdapter) {} public async getTagsWithIds(tagIds: string[]): Promise { - return await this.REST.get(`/api/beats/tags/${tagIds.join(',')}`); + return (await this.REST.get<{ tags: BeatTag[] }>(`/api/beats/tags/${tagIds.join(',')}`)).tags; } public async getAll(): Promise { - return await this.REST.get(`/api/beats/tags`); + return (await this.REST.get<{ tags: BeatTag[] }>(`/api/beats/tags`)).tags; } public async upsertTag(tag: BeatTag): Promise { - return await this.REST.put(`/api/beats/tag/{tag}`, { + return (await this.REST.put<{ tag: BeatTag }>(`/api/beats/tag/{tag}`, { configuration_blocks: tag.configuration_blocks, - }); + })).tag; } } diff --git a/x-pack/plugins/beats_management/public/lib/adapters/tokens/adapter_types.ts b/x-pack/plugins/beats_management/public/lib/adapters/tokens/adapter_types.ts index 43f2d95b02b2c..55b7e6f94fe04 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/tokens/adapter_types.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/tokens/adapter_types.ts @@ -3,11 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -export interface TokenEnrollmentData { - token: string | null; - expires_on: string; -} export interface CMTokensAdapter { - createEnrollmentToken(): Promise; + createEnrollmentToken(): Promise; } diff --git a/x-pack/plugins/beats_management/public/lib/adapters/tokens/memory_tokens_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/tokens/memory_tokens_adapter.ts index 4e80bc234d186..f329e491c9ad0 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/tokens/memory_tokens_adapter.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/tokens/memory_tokens_adapter.ts @@ -4,13 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CMTokensAdapter, TokenEnrollmentData } from './adapter_types'; +import { CMTokensAdapter } from './adapter_types'; export class MemoryTokensAdapter implements CMTokensAdapter { - public async createEnrollmentToken(): Promise { - return { - token: '2jnwkrhkwuehriauhweair', - expires_on: new Date().toJSON(), - }; + public async createEnrollmentToken(): Promise { + return '2jnwkrhkwuehriauhweair'; } } diff --git a/x-pack/plugins/beats_management/public/lib/adapters/tokens/rest_tokens_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/tokens/rest_tokens_adapter.ts index de5890fdaebf5..778bcbf5d8d5c 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/tokens/rest_tokens_adapter.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/tokens/rest_tokens_adapter.ts @@ -5,13 +5,14 @@ */ import { RestAPIAdapter } from '../rest_api/adapter_types'; -import { CMTokensAdapter, TokenEnrollmentData } from './adapter_types'; +import { CMTokensAdapter } from './adapter_types'; export class RestTokensAdapter implements CMTokensAdapter { constructor(private readonly REST: RestAPIAdapter) {} - public async createEnrollmentToken(): Promise { - const tokens = await this.REST.post('/api/beats/enrollment_tokens'); + public async createEnrollmentToken(): Promise { + const tokens = (await this.REST.post<{ tokens: string[] }>('/api/beats/enrollment_tokens')) + .tokens; return tokens[0]; } } diff --git a/x-pack/plugins/beats_management/public/pages/main/beats.tsx b/x-pack/plugins/beats_management/public/pages/main/beats.tsx index 1d8a9e6717605..24baf43ba2c83 100644 --- a/x-pack/plugins/beats_management/public/pages/main/beats.tsx +++ b/x-pack/plugins/beats_management/public/pages/main/beats.tsx @@ -7,15 +7,7 @@ import { // @ts-ignore typings for EuiBadge not present in current version EuiBadge, - EuiButton, - EuiButtonEmpty, EuiFlexItem, - EuiModal, - EuiModalBody, - EuiModalFooter, - EuiModalHeader, - EuiModalHeaderTitle, - EuiOverlayMask, } from '@elastic/eui'; import React from 'react'; @@ -23,9 +15,11 @@ import { BeatTag, CMBeat, CMPopulatedBeat } from '../../../common/domain_types'; import { BeatsTagAssignment } from '../../../server/lib/adapters/beats/adapter_types'; import { BeatsTableType, Table } from '../../components/table'; import { FrontendLibs } from '../../lib/lib'; +import { BeatsActionArea } from './beats_action_area'; interface BeatsPageProps { libs: FrontendLibs; + location: any; } interface BeatsPageState { @@ -35,45 +29,7 @@ interface BeatsPageState { } export class BeatsPage extends React.PureComponent { - public static ActionArea = ({ match, history }: { match: any; history: any }) => ( -
- { - window.alert('This will later go to more general beats install instructions.'); - window.location.href = '#/home/tutorial/dockerMetrics'; - }} - > - Learn how to install beats - - { - history.push('/beats/enroll/foobar'); - }} - > - Enroll Beats - - - {match.params.enrollmentToken != null && ( - - history.push('/beats')} style={{ width: '800px' }}> - - Enroll Beats - - - - Enrollment UI here for enrollment token of: {match.params.enrollmentToken} - - - - history.push('/beats')}>Cancel - - - - )} -
- ); + public static ActionArea = BeatsActionArea; constructor(props: BeatsPageProps) { super(props); @@ -85,13 +41,18 @@ export class BeatsPage extends React.PureComponent diff --git a/x-pack/plugins/beats_management/public/pages/main/beats_action_area.tsx b/x-pack/plugins/beats_management/public/pages/main/beats_action_area.tsx new file mode 100644 index 0000000000000..6cedb5bd363b9 --- /dev/null +++ b/x-pack/plugins/beats_management/public/pages/main/beats_action_area.tsx @@ -0,0 +1,162 @@ +/* + * 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 { + // @ts-ignore typings for EuiBasicTable not present in current version + EuiBasicTable, + EuiButton, + EuiButtonEmpty, + EuiLoadingSpinner, + EuiModal, + EuiModalBody, + EuiModalHeader, + EuiModalHeaderTitle, + EuiOverlayMask +} from '@elastic/eui'; +import React from 'react'; +import { CMBeat } from '../../../common/domain_types'; +import { FrontendLibs } from '../../lib/lib'; + +export class BeatsActionArea extends React.Component { + private pinging = false + constructor(props: any) { + super(props) + + this.state = { + enrolledBeat: null + } + } + public pingForBeatWithToken = async(libs: FrontendLibs,token: string): Promise => { + try { + const beats = await libs.beats.getBeatWithToken(token); + if(!beats) { throw new Error('no beats') } + return beats; + } catch(err) { + if(this.pinging) { + const timeout = (ms:number) => new Promise(res => setTimeout(res, ms)) + await timeout(5000) + return await this.pingForBeatWithToken(libs, token); + } + } + } + public async componentDidMount() { + if(this.props.match.params.enrollmentToken) { + this.waitForToken(this.props.match.params.enrollmentToken) + } + } + public waitForToken = async (token: string) => { + this.pinging = true; + const enrolledBeat = await this.pingForBeatWithToken(this.props.libs, token) as CMBeat; + this.setState({ + enrolledBeat + }) + this.pinging = false + } + public render() { + const { + match, + history, + libs, + } = this.props + return ( +
+ { + window.alert('This will later go to more general beats install instructions.'); + window.location.href = '#/home/tutorial/dockerMetrics'; + }} + > + Learn how to install beats + + { + const token = await libs.tokens.createEnrollmentToken(); + history.push(`/beats/enroll/${token}`); + this.waitForToken(token); + }} + > + Enroll Beats + + + {match.params.enrollmentToken != null && ( + + { + this.pinging = false; + this.setState({ + enrolledBeat: null + }, () => history.push('/beats')) + }} style={{ width: '640px' }}> + + Enroll a new Beat + + {!this.state.enrolledBeat && ( + + To enroll a Beat with Centeral Management, run this command on the host that has Beats + installed. +
+
+
+
+
+ $ beats enroll {window.location.protocol}//{window.location.host} {match.params.enrollmentToken} +
+
+
+
+ +
+
+ Waiting for enroll command to be run... + +
+ )} + {this.state.enrolledBeat && ( + + A Beat was enrolled with the following data: +
+
+
+ +
+
+ { + this.setState({ + enrolledBeat: null + }) + const token = await libs.tokens.createEnrollmentToken(); + history.push(`/beats/enroll/${token}`); + this.waitForToken(token); + }} + > + Enroll Another Beat + +
+ )} + +
+
+ )} +
+)}} \ No newline at end of file diff --git a/x-pack/plugins/beats_management/public/pages/main/index.tsx b/x-pack/plugins/beats_management/public/pages/main/index.tsx index f18e5e05b5747..49c4f757079cd 100644 --- a/x-pack/plugins/beats_management/public/pages/main/index.tsx +++ b/x-pack/plugins/beats_management/public/pages/main/index.tsx @@ -38,18 +38,6 @@ export class MainPages extends React.PureComponent { - if (this.state.enrollBeat) { - return this.setState({ - enrollBeat: null, - }); - } - - // TODO: create a real enromment token - return this.setState({ - enrollBeat: { enrollmentToken: '5g3i4ug5uy34g' }, - }); - }; public onSelectedTabChanged = (id: string) => { this.props.history.push(id); @@ -89,7 +77,10 @@ export class MainPages extends React.PureComponent - + } + /> } > diff --git a/x-pack/plugins/beats_management/readme.md b/x-pack/plugins/beats_management/readme.md index fdd56a393e573..4fca7e635fcfc 100644 --- a/x-pack/plugins/beats_management/readme.md +++ b/x-pack/plugins/beats_management/readme.md @@ -1,7 +1,16 @@ # Documentation for Beats CM in x-pack kibana +Notes: +Falure to have auth enabled in Kibana will make for a broken UI. UI based errors not yet in place + ### Run tests ``` node scripts/jest.js plugins/beats --watch ``` + +### Run command to fake an enrolling beat (from beats_management dir) + +``` +node scripts/enroll.js +``` diff --git a/x-pack/plugins/beats_management/scripts/enroll.js b/x-pack/plugins/beats_management/scripts/enroll.js new file mode 100644 index 0000000000000..996ece4136605 --- /dev/null +++ b/x-pack/plugins/beats_management/scripts/enroll.js @@ -0,0 +1,34 @@ +/* + * 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. + */ +const request = require('request'); +const Chance = require('chance'); // eslint-disable-line +const args = process.argv.slice(2); +const chance = new Chance(); + +const enroll = async token => { + const beatId = chance.word(); + + await request( + { + url: `http://localhost:5601/api/beats/agent/${beatId}`, + method: 'POST', + headers: { + 'kbn-xsrf': 'xxx', + 'kbn-beats-enrollment-token': token, + }, + body: JSON.stringify({ + type: 'filebeat', + host_name: `${chance.word()}.bar.com`, + version: '6.3.0', + }), + }, + (error, response, body) => { + console.log(error, body); + } + ); +}; + +enroll(args[0]); diff --git a/x-pack/plugins/beats_management/server/lib/adapters/beats/adapter_types.ts b/x-pack/plugins/beats_management/server/lib/adapters/beats/adapter_types.ts index 1f1f2fd13e3bc..aa12674bf25b7 100644 --- a/x-pack/plugins/beats_management/server/lib/adapters/beats/adapter_types.ts +++ b/x-pack/plugins/beats_management/server/lib/adapters/beats/adapter_types.ts @@ -10,9 +10,10 @@ import { FrameworkUser } from '../framework/adapter_types'; export interface CMBeatsAdapter { insert(beat: CMBeat): Promise; update(beat: CMBeat): Promise; - get(user: FrameworkUser, id: string): Promise; - getAll(user: FrameworkUser): Promise; - getWithIds(user: FrameworkUser, beatIds: string[]): Promise; + get(user: FrameworkUser, id: string): Promise; + getAll(user: FrameworkUser): Promise; + getWithIds(user: FrameworkUser, beatIds: string[]): Promise; + getBeatWithToken(user: FrameworkUser, enrollmentToken: string): Promise; removeTagsFromBeats( user: FrameworkUser, removals: BeatsTagAssignment[] diff --git a/x-pack/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts index 1d00f95e3a4e0..dbe16067dc6f2 100644 --- a/x-pack/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts +++ b/x-pack/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts @@ -34,7 +34,7 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { return null; } - return _get(response, '_source.beat'); + return _get(response, '_source.beat'); } public async insert(beat: CMBeat) { @@ -86,6 +86,31 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { .map((b: any) => b._source.beat); } + public async getBeatWithToken( + user: FrameworkUser, + enrollmentToken: string + ): Promise { + const params = { + ignore: [404], + index: INDEX_NAMES.BEATS, + type: '_doc', + body: { + query: { + match: { 'beat.enrollment_token': enrollmentToken }, + }, + }, + }; + + const response = await this.database.search(user, params); + + const beats = _get(response, 'hits.hits', []); + + if (beats.length === 0) { + return null; + } + return _get(beats[0], '_source.beat'); + } + public async getAll(user: FrameworkUser) { const params = { index: INDEX_NAMES.BEATS, diff --git a/x-pack/plugins/beats_management/server/lib/adapters/beats/memory_beats_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/beats/memory_beats_adapter.ts index 1de1fe71e54ea..350d88ab6cada 100644 --- a/x-pack/plugins/beats_management/server/lib/adapters/beats/memory_beats_adapter.ts +++ b/x-pack/plugins/beats_management/server/lib/adapters/beats/memory_beats_adapter.ts @@ -38,8 +38,15 @@ export class MemoryBeatsAdapter implements CMBeatsAdapter { return this.beatsDB.filter(beat => beatIds.includes(beat.id)); } + public async getBeatWithToken( + user: FrameworkUser, + enrollmentToken: string + ): Promise { + return this.beatsDB.find(beat => enrollmentToken === beat.enrollment_token) || null; + } + public async getAll(user: FrameworkUser) { - return this.beatsDB.map((beat: any) => omit(beat, ['access_token'])); + return this.beatsDB.map((beat: any) => omit(beat, ['access_token'])); } public async removeTagsFromBeats( diff --git a/x-pack/plugins/beats_management/server/lib/domains/beats.ts b/x-pack/plugins/beats_management/server/lib/domains/beats.ts index f3c98b056d5e1..2d81b0eb05950 100644 --- a/x-pack/plugins/beats_management/server/lib/domains/beats.ts +++ b/x-pack/plugins/beats_management/server/lib/domains/beats.ts @@ -39,6 +39,10 @@ export class CMBeatsDomain { return await this.adapter.get(user, beatId); } + public async getByEnrollmentToken(user: FrameworkUser, enrollmentToken: string) { + return await this.adapter.getBeatWithToken(user, enrollmentToken); + } + public async update(beatId: string, accessToken: string, beatData: Partial) { const beat = await this.adapter.get(this.framework.internalUser, beatId); @@ -83,6 +87,7 @@ export class CMBeatsDomain { await this.adapter.insert({ ...beat, + enrollment_token: enrollmentToken, verified_on: verifiedOn, access_token: accessToken, host_ip: remoteAddress, diff --git a/x-pack/plugins/beats_management/server/management_server.ts b/x-pack/plugins/beats_management/server/management_server.ts index f7316ea0887ff..2cc4676da359e 100644 --- a/x-pack/plugins/beats_management/server/management_server.ts +++ b/x-pack/plugins/beats_management/server/management_server.ts @@ -7,10 +7,13 @@ import { CMServerLibs } from './lib/lib'; import { createGetBeatConfigurationRoute } from './rest_api/beats/configuration'; import { createBeatEnrollmentRoute } from './rest_api/beats/enroll'; +import { createGetBeatRoute } from './rest_api/beats/get'; import { createListAgentsRoute } from './rest_api/beats/list'; import { createTagAssignmentsRoute } from './rest_api/beats/tag_assignment'; import { createTagRemovalsRoute } from './rest_api/beats/tag_removal'; import { createBeatUpdateRoute } from './rest_api/beats/update'; +import { createGetTagsWithIdsRoute } from './rest_api/tags/get'; +import { createListTagsRoute } from './rest_api/tags/list'; import { createSetTagRoute } from './rest_api/tags/set'; import { createTokensRoute } from './rest_api/tokens/create'; import { beatsIndexTemplate } from './utils/index_templates'; @@ -23,6 +26,9 @@ export const initManagementServer = (libs: CMServerLibs) => { }); } + libs.framework.registerRoute(createGetBeatRoute(libs)); + libs.framework.registerRoute(createGetTagsWithIdsRoute(libs)); + libs.framework.registerRoute(createListTagsRoute(libs)); libs.framework.registerRoute(createGetBeatConfigurationRoute(libs)); libs.framework.registerRoute(createTagAssignmentsRoute(libs)); libs.framework.registerRoute(createListAgentsRoute(libs)); diff --git a/x-pack/plugins/beats_management/server/rest_api/beats/get.ts b/x-pack/plugins/beats_management/server/rest_api/beats/get.ts index e7ed5ef5c2cc8..fc0437bce8329 100644 --- a/x-pack/plugins/beats_management/server/rest_api/beats/get.ts +++ b/x-pack/plugins/beats_management/server/rest_api/beats/get.ts @@ -4,32 +4,36 @@ * you may not use this file except in compliance with the Elastic License. */ -import Joi from 'joi'; import { CMBeat } from '../../../common/domain_types'; import { CMServerLibs } from '../../lib/lib'; import { wrapEsError } from '../../utils/error_wrappers'; export const createGetBeatRoute = (libs: CMServerLibs) => ({ method: 'GET', - path: '/api/beats/agent/{beatId}', - config: { - validate: { - headers: Joi.object({ - 'kbn-beats-access-token': Joi.string().required(), - }).options({ allowUnknown: true }), - }, - }, + path: '/api/beats/agent/{beatId}/{token?}', + handler: async (request: any, reply: any) => { const beatId = request.params.beatId; - let beat: CMBeat; - try { - beat = await libs.beats.getById(request.user, beatId); - if (beat === null) { - return reply({ message: 'Beat not found' }).code(404); + let beat: CMBeat | null; + if (beatId === 'unknown') { + try { + beat = await libs.beats.getByEnrollmentToken(request.user, request.params.token); + if (beat === null) { + return reply().code(200); + } + } catch (err) { + return reply(wrapEsError(err)); + } + } else { + try { + beat = await libs.beats.getById(request.user, beatId); + if (beat === null) { + return reply({ message: 'Beat not found' }).code(404); + } + } catch (err) { + return reply(wrapEsError(err)); } - } catch (err) { - return reply(wrapEsError(err)); } delete beat.access_token; diff --git a/x-pack/plugins/beats_management/server/utils/index_templates/beats_template.json b/x-pack/plugins/beats_management/server/utils/index_templates/beats_template.json index 0d00abbc5d759..c77d8f9719680 100644 --- a/x-pack/plugins/beats_management/server/utils/index_templates/beats_template.json +++ b/x-pack/plugins/beats_management/server/utils/index_templates/beats_template.json @@ -30,6 +30,9 @@ "id": { "type": "keyword" }, + "color": { + "type": "keyword" + }, "configuration_blocks": { "type": "nested", "properties": { @@ -48,6 +51,9 @@ "id": { "type": "keyword" }, + "enrollment_token": { + "type": "keyword" + }, "access_token": { "type": "keyword" }, @@ -75,7 +81,7 @@ "tags": { "type": "keyword" }, - "central_configuration_yml": { + "orig_configuration_yml": { "type": "text" }, "metadata": { diff --git a/x-pack/test/api_integration/apis/beats/enroll_beat.js b/x-pack/test/api_integration/apis/beats/enroll_beat.js index 0234359b4f9ff..61c9ec79eb74b 100644 --- a/x-pack/test/api_integration/apis/beats/enroll_beat.js +++ b/x-pack/test/api_integration/apis/beats/enroll_beat.js @@ -52,7 +52,7 @@ export default function ({ getService }) { }); }); - it('should enroll beat in an unverified state', async () => { + it('should enroll beat in a verified state', async () => { await supertest .post(`/api/beats/agent/${beatId}`) .set('kbn-xsrf', 'xxx') From 1e8415d015938040afa7f689b3f444638081d04b Mon Sep 17 00:00:00 2001 From: Matt Apperson Date: Mon, 6 Aug 2018 14:28:01 -0400 Subject: [PATCH 30/43] add update on client side, expand update on server to allow for partial data, and user auth --- .../lib/adapters/beats/adapter_types.ts | 1 + .../adapters/beats/memory_beats_adapter.ts | 11 +++++ .../lib/adapters/beats/rest_beats_adapter.ts | 4 ++ .../lib/adapters/beats/adapter_types.ts | 4 +- .../beats/elasticsearch_beats_adapter.ts | 45 +++++++++---------- .../adapters/beats/memory_beats_adapter.ts | 4 +- .../server/lib/compose/kibana.ts | 3 +- .../__tests__/beats/remove_tags.test.ts | 6 ++- .../server/lib/domains/beats.ts | 26 ++++++----- .../beats_management/server/lib/lib.ts | 4 +- .../server/rest_api/beats/update.ts | 29 +++++++++--- 11 files changed, 87 insertions(+), 50 deletions(-) diff --git a/x-pack/plugins/beats_management/public/lib/adapters/beats/adapter_types.ts b/x-pack/plugins/beats_management/public/lib/adapters/beats/adapter_types.ts index 38129f27ab38f..c47ea7a4c6350 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/beats/adapter_types.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/beats/adapter_types.ts @@ -8,6 +8,7 @@ import { CMBeat } from '../../../../common/domain_types'; export interface CMBeatsAdapter { get(id: string): Promise; + update(id: string, beatData: Partial): Promise; getAll(): Promise; removeTagsFromBeats(removals: BeatsTagAssignment[]): Promise; assignTagsToBeats(assignments: BeatsTagAssignment[]): Promise; diff --git a/x-pack/plugins/beats_management/public/lib/adapters/beats/memory_beats_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/beats/memory_beats_adapter.ts index 565ed0932214f..b32ccff660249 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/beats/memory_beats_adapter.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/beats/memory_beats_adapter.ts @@ -25,6 +25,17 @@ export class MemoryBeatsAdapter implements CMBeatsAdapter { return this.beatsDB.find(beat => beat.id === id) || null; } + public async update(id: string, beatData: Partial): Promise { + const index = this.beatsDB.findIndex(beat => beat.id === id); + + if (index === -1) { + return false; + } + + this.beatsDB[index] = { ...this.beatsDB[index], ...beatData }; + return true; + } + public async getAll() { return this.beatsDB.map((beat: any) => omit(beat, ['access_token'])); } diff --git a/x-pack/plugins/beats_management/public/lib/adapters/beats/rest_beats_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/beats/rest_beats_adapter.ts index aaf9d545dffee..1cf134b8293a4 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/beats/rest_beats_adapter.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/beats/rest_beats_adapter.ts @@ -28,6 +28,10 @@ export class RestBeatsAdapter implements CMBeatsAdapter { return (await this.REST.get<{ beats: CMBeat[] }>('/api/beats/agents')).beats; } + public async update(id: string, beatData: Partial): Promise { + return (await this.REST.put<{ success: true }>(`/api/beats/agent/${id}`, beatData)).success; + } + public async removeTagsFromBeats(removals: BeatsTagAssignment[]): Promise { return (await this.REST.post<{ removals: BeatsRemovalReturn[] }>( `/api/beats/agents_tags/removals`, diff --git a/x-pack/plugins/beats_management/server/lib/adapters/beats/adapter_types.ts b/x-pack/plugins/beats_management/server/lib/adapters/beats/adapter_types.ts index aa12674bf25b7..9fc2578ccf8da 100644 --- a/x-pack/plugins/beats_management/server/lib/adapters/beats/adapter_types.ts +++ b/x-pack/plugins/beats_management/server/lib/adapters/beats/adapter_types.ts @@ -8,8 +8,8 @@ import { CMBeat } from '../../../../common/domain_types'; import { FrameworkUser } from '../framework/adapter_types'; export interface CMBeatsAdapter { - insert(beat: CMBeat): Promise; - update(beat: CMBeat): Promise; + insert(user: FrameworkUser, beat: CMBeat): Promise; + update(user: FrameworkUser, beat: CMBeat): Promise; get(user: FrameworkUser, id: string): Promise; getAll(user: FrameworkUser): Promise; getWithIds(user: FrameworkUser, beatIds: string[]): Promise; diff --git a/x-pack/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts index dbe16067dc6f2..036d5d5a23e23 100644 --- a/x-pack/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts +++ b/x-pack/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts @@ -8,17 +8,14 @@ import { flatten, get as _get, omit } from 'lodash'; import { INDEX_NAMES } from '../../../../common/constants'; import { CMBeat } from '../../../../common/domain_types'; import { DatabaseAdapter } from '../database/adapter_types'; -import { BackendFrameworkAdapter } from '../framework/adapter_types'; import { FrameworkUser } from '../framework/adapter_types'; import { BeatsTagAssignment, CMBeatsAdapter } from './adapter_types'; export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { private database: DatabaseAdapter; - private framework: BackendFrameworkAdapter; - constructor(database: DatabaseAdapter, framework: BackendFrameworkAdapter) { + constructor(database: DatabaseAdapter) { this.database = database; - this.framework = framework; } public async get(user: FrameworkUser, id: string) { @@ -37,13 +34,13 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { return _get(response, '_source.beat'); } - public async insert(beat: CMBeat) { + public async insert(user: FrameworkUser, beat: CMBeat) { const body = { beat, type: 'beat', }; - await this.database.create(this.framework.internalUser, { + await this.database.create(user, { body, id: `beat:${beat.id}`, index: INDEX_NAMES.BEATS, @@ -52,7 +49,7 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { }); } - public async update(beat: CMBeat) { + public async update(user: FrameworkUser, beat: CMBeat) { const body = { beat, type: 'beat', @@ -65,7 +62,7 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { refresh: 'wait_for', type: '_doc', }; - await this.database.index(this.framework.internalUser, params); + await this.database.index(user, params); } public async getWithIds(user: FrameworkUser, beatIds: string[]) { @@ -129,16 +126,15 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { ): Promise { const body = flatten( removals.map(({ beatId, tag }) => { - const script = - '' + - 'def beat = ctx._source.beat; ' + - 'if (beat.tags != null) { ' + - ' beat.tags.removeAll([params.tag]); ' + - '}'; + const script = ` + def beat = ctx._source.beat; + if (beat.tags != null) { + beat.tags.removeAll([params.tag]); + }`; return [ { update: { _id: `beat:${beatId}` } }, - { script: { source: script, params: { tag } } }, + { script: { source: script.replace(' ', ''), params: { tag } } }, ]; }) ); @@ -162,19 +158,18 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { ): Promise { const body = flatten( assignments.map(({ beatId, tag }) => { - const script = - '' + - 'def beat = ctx._source.beat; ' + - 'if (beat.tags == null) { ' + - ' beat.tags = []; ' + - '} ' + - 'if (!beat.tags.contains(params.tag)) { ' + - ' beat.tags.add(params.tag); ' + - '}'; + const script = ` + def beat = ctx._source.beat; + if (beat.tags == null) { + beat.tags = []; + } + if (!beat.tags.contains(params.tag)) { + beat.tags.add(params.tag); + }`; return [ { update: { _id: `beat:${beatId}` } }, - { script: { source: script, params: { tag } } }, + { script: { source: script.replace(' ', ''), params: { tag } } }, ]; }) ); diff --git a/x-pack/plugins/beats_management/server/lib/adapters/beats/memory_beats_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/beats/memory_beats_adapter.ts index 350d88ab6cada..3ab38a0716455 100644 --- a/x-pack/plugins/beats_management/server/lib/adapters/beats/memory_beats_adapter.ts +++ b/x-pack/plugins/beats_management/server/lib/adapters/beats/memory_beats_adapter.ts @@ -21,11 +21,11 @@ export class MemoryBeatsAdapter implements CMBeatsAdapter { return this.beatsDB.find(beat => beat.id === id) || null; } - public async insert(beat: CMBeat) { + public async insert(user: FrameworkUser, beat: CMBeat) { this.beatsDB.push(beat); } - public async update(beat: CMBeat) { + public async update(user: FrameworkUser, beat: CMBeat) { const beatIndex = this.beatsDB.findIndex(b => b.id === beat.id); this.beatsDB[beatIndex] = { diff --git a/x-pack/plugins/beats_management/server/lib/compose/kibana.ts b/x-pack/plugins/beats_management/server/lib/compose/kibana.ts index 685171669a887..bc00278251610 100644 --- a/x-pack/plugins/beats_management/server/lib/compose/kibana.ts +++ b/x-pack/plugins/beats_management/server/lib/compose/kibana.ts @@ -25,9 +25,10 @@ export function compose(server: any): CMServerLibs { const tokens = new CMTokensDomain(new ElasticsearchTokensAdapter(database, framework), { framework, }); - const beats = new CMBeatsDomain(new ElasticsearchBeatsAdapter(database, framework), { + const beats = new CMBeatsDomain(new ElasticsearchBeatsAdapter(database), { tags, tokens, + framework, }); const domainLibs: CMDomainLibs = { diff --git a/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/remove_tags.test.ts b/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/remove_tags.test.ts index f75334e917c2b..490414ae99cd5 100644 --- a/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/remove_tags.test.ts +++ b/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/remove_tags.test.ts @@ -5,9 +5,9 @@ */ import { BeatTag, CMBeat } from '../../../../../common/domain_types'; +import { FrameworkInternalUser } from '../../../adapters/framework/adapter_types'; import { compose } from '../../../compose/testing'; import { CMServerLibs } from '../../../lib'; -import { FrameworkInternalUser } from './../../../adapters/framework/adapter_types'; const internalUser: FrameworkInternalUser = { kind: 'internal' }; @@ -21,6 +21,7 @@ describe('Beats Domain Lib', () => { beatsDB = [ { access_token: '9a6c99ae0fd84b068819701169cd8a4b', + enrollment_token: '1234', host_ip: '1.2.3.4', host_name: 'foo.bar.com', id: 'qux', @@ -28,6 +29,7 @@ describe('Beats Domain Lib', () => { }, { access_token: '188255eb560a4448b72656c5e99cae6f', + enrollment_token: '1234', host_ip: '22.33.11.44', host_name: 'baz.bar.com', id: 'baz', @@ -35,6 +37,7 @@ describe('Beats Domain Lib', () => { }, { access_token: '93c4a4dd08564c189a7ec4e4f046b975', + enrollment_token: '1234', host_ip: '1.2.3.4', host_name: 'foo.bar.com', id: 'foo', @@ -44,6 +47,7 @@ describe('Beats Domain Lib', () => { }, { access_token: '3c4a4dd08564c189a7ec4e4f046b9759', + enrollment_token: '1234', host_ip: '11.22.33.44', host_name: 'foo.com', id: 'bar', diff --git a/x-pack/plugins/beats_management/server/lib/domains/beats.ts b/x-pack/plugins/beats_management/server/lib/domains/beats.ts index 2d81b0eb05950..49b0ad5543f65 100644 --- a/x-pack/plugins/beats_management/server/lib/domains/beats.ts +++ b/x-pack/plugins/beats_management/server/lib/domains/beats.ts @@ -14,7 +14,7 @@ import { FrameworkUser } from '../adapters/framework/adapter_types'; import { CMAssignmentReturn } from '../adapters/beats/adapter_types'; import { BeatsRemovalReturn } from '../adapters/beats/adapter_types'; -import { BeatEnrollmentStatus, CMDomainLibs, CMServerLibs } from '../lib'; +import { BeatEnrollmentStatus, CMDomainLibs, CMServerLibs, UserOrToken } from '../lib'; export class CMBeatsDomain { private tags: CMDomainLibs['tags']; @@ -43,24 +43,26 @@ export class CMBeatsDomain { return await this.adapter.getBeatWithToken(user, enrollmentToken); } - public async update(beatId: string, accessToken: string, beatData: Partial) { + public async update(userOrToken: UserOrToken, beatId: string, beatData: Partial) { const beat = await this.adapter.get(this.framework.internalUser, beatId); - - const { verified: isAccessTokenValid } = this.tokens.verifyToken( - beat ? beat.access_token : '', - accessToken - ); - // TODO make return type enum if (beat === null) { return 'beat-not-found'; } - if (!isAccessTokenValid) { - return 'invalid-access-token'; + if (typeof userOrToken === 'string') { + const { verified: isAccessTokenValid } = this.tokens.verifyToken( + beat ? beat.access_token : '', + userOrToken + ); + if (!isAccessTokenValid) { + return 'invalid-access-token'; + } } - await this.adapter.update({ + const user = typeof userOrToken === 'string' ? this.framework.internalUser : userOrToken; + + await this.adapter.update(user, { ...beat, ...beatData, }); @@ -85,7 +87,7 @@ export class CMBeatsDomain { const accessToken = this.tokens.generateAccessToken(); const verifiedOn = moment().toJSON(); - await this.adapter.insert({ + await this.adapter.insert(this.framework.internalUser, { ...beat, enrollment_token: enrollmentToken, verified_on: verifiedOn, diff --git a/x-pack/plugins/beats_management/server/lib/lib.ts b/x-pack/plugins/beats_management/server/lib/lib.ts index 824c5722a4bd8..50db95a405920 100644 --- a/x-pack/plugins/beats_management/server/lib/lib.ts +++ b/x-pack/plugins/beats_management/server/lib/lib.ts @@ -5,11 +5,13 @@ */ import { DatabaseAdapter } from './adapters/database/adapter_types'; -import { BackendFrameworkAdapter } from './adapters/framework/adapter_types'; +import { BackendFrameworkAdapter, FrameworkUser } from './adapters/framework/adapter_types'; import { CMBeatsDomain } from './domains/beats'; import { CMTagsDomain } from './domains/tags'; import { CMTokensDomain } from './domains/tokens'; +export type UserOrToken = FrameworkUser | string; + export interface CMDomainLibs { beats: CMBeatsDomain; tags: CMTagsDomain; diff --git a/x-pack/plugins/beats_management/server/rest_api/beats/update.ts b/x-pack/plugins/beats_management/server/rest_api/beats/update.ts index 9c8e7daf475f8..0bd40239270fe 100644 --- a/x-pack/plugins/beats_management/server/rest_api/beats/update.ts +++ b/x-pack/plugins/beats_management/server/rest_api/beats/update.ts @@ -15,7 +15,9 @@ export const createBeatUpdateRoute = (libs: CMServerLibs) => ({ method: 'PUT', path: '/api/beats/agent/{beatId}', config: { - auth: false, + auth: { + mode: 'optional', + }, validate: { headers: Joi.object({ 'kbn-beats-access-token': Joi.string(), @@ -26,34 +28,49 @@ export const createBeatUpdateRoute = (libs: CMServerLibs) => ({ beatId: Joi.string(), }), payload: Joi.object({ + active: Joi.bool(), ephemeral_id: Joi.string(), host_name: Joi.string(), local_configuration_yml: Joi.string(), metadata: Joi.object(), type: Joi.string(), version: Joi.string(), - }).required(), + }), }, }, handler: async (request: FrameworkRequest, reply: any) => { const { beatId } = request.params; const accessToken = request.headers['kbn-beats-access-token']; const remoteAddress = request.info.remoteAddress; + const userOrToken = accessToken || request.user; + + if (request.user.kind === 'unauthenticated' && request.payload.active !== undefined) { + return reply({ message: 'access-token is not a valid auth type to change beat status' }).code( + 401 + ); + } try { - const status = await libs.beats.update(beatId, accessToken, { + const beat = await libs.beats.getById(libs.framework.internalUser, beatId); + + if (beat === null) { + return reply({ message: 'Beat not found' }).code(404); + } + + const status = await libs.beats.update(userOrToken, beatId, { + ...beat, ...request.payload, host_ip: remoteAddress, }); switch (status) { case 'beat-not-found': - return reply({ message: 'Beat not found' }).code(404); + return reply({ message: 'Beat not found', success: false }).code(404); case 'invalid-access-token': - return reply({ message: 'Invalid access token' }).code(401); + return reply({ message: 'Invalid access token', success: false }).code(401); } - reply().code(204); + reply({ success: true }).code(204); } catch (err) { return reply(wrapEsError(err)); } From d7d68395bcaba06bef4ff041e0337e26d7437b58 Mon Sep 17 00:00:00 2001 From: Matt Apperson Date: Mon, 6 Aug 2018 14:29:58 -0400 Subject: [PATCH 31/43] remove double beat lookup --- .../beats_management/server/rest_api/beats/update.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/x-pack/plugins/beats_management/server/rest_api/beats/update.ts b/x-pack/plugins/beats_management/server/rest_api/beats/update.ts index 0bd40239270fe..bf94f83ec22d7 100644 --- a/x-pack/plugins/beats_management/server/rest_api/beats/update.ts +++ b/x-pack/plugins/beats_management/server/rest_api/beats/update.ts @@ -51,14 +51,7 @@ export const createBeatUpdateRoute = (libs: CMServerLibs) => ({ } try { - const beat = await libs.beats.getById(libs.framework.internalUser, beatId); - - if (beat === null) { - return reply({ message: 'Beat not found' }).code(404); - } - const status = await libs.beats.update(userOrToken, beatId, { - ...beat, ...request.payload, host_ip: remoteAddress, }); From e9dab953c989d3d5a5c44d173b01783475de9331 Mon Sep 17 00:00:00 2001 From: Matt Apperson Date: Mon, 6 Aug 2018 14:33:05 -0400 Subject: [PATCH 32/43] fix tests --- x-pack/plugins/beats_management/server/lib/compose/testing.ts | 1 + .../server/lib/domains/__tests__/beats/assign_tags.test.ts | 1 + .../server/lib/domains/__tests__/beats/enroll.test.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/x-pack/plugins/beats_management/server/lib/compose/testing.ts b/x-pack/plugins/beats_management/server/lib/compose/testing.ts index 25b6776b6908b..7928891443708 100644 --- a/x-pack/plugins/beats_management/server/lib/compose/testing.ts +++ b/x-pack/plugins/beats_management/server/lib/compose/testing.ts @@ -36,6 +36,7 @@ export function compose({ const beats = new CMBeatsDomain(new MemoryBeatsAdapter(beatsDB), { tags, tokens, + framework, }); const domainLibs: CMDomainLibs = { diff --git a/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/assign_tags.test.ts b/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/assign_tags.test.ts index 20f4a7d36e4f0..040e48feda7ed 100644 --- a/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/assign_tags.test.ts +++ b/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/assign_tags.test.ts @@ -93,6 +93,7 @@ describe('Beats Domain Lib', () => { beatsLib = new CMBeatsDomain(new MemoryBeatsAdapter(beatsDB), { tags: tagsLib, tokens: tokensLib, + framework, }); }); diff --git a/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/enroll.test.ts b/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/enroll.test.ts index c5bc0935dc6cc..5f8a0b683a5b5 100644 --- a/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/enroll.test.ts +++ b/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/enroll.test.ts @@ -81,6 +81,7 @@ describe('Beats Domain Lib', () => { beatsLib = new CMBeatsDomain(new MemoryBeatsAdapter(beatsDB), { tags: tagsLib, tokens: tokensLib, + framework, }); }); From ad79998ce5bb4ed8e4fa4431bba0bfa3df55c6dd Mon Sep 17 00:00:00 2001 From: Matt Apperson Date: Mon, 6 Aug 2018 14:41:19 -0400 Subject: [PATCH 33/43] only return active beats --- .../beats_management/common/domain_types.ts | 1 + .../beats_management/server/lib/domains/beats.ts | 16 +++++++++------- .../server/rest_api/beats/list.ts | 2 +- .../utils/index_templates/beats_template.json | 3 +++ 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/beats_management/common/domain_types.ts b/x-pack/plugins/beats_management/common/domain_types.ts index b943cc2cedc2b..00feee423e262 100644 --- a/x-pack/plugins/beats_management/common/domain_types.ts +++ b/x-pack/plugins/beats_management/common/domain_types.ts @@ -13,6 +13,7 @@ export interface ConfigurationBlock { export interface CMBeat { id: string; enrollment_token: string; + active: boolean; access_token: string; verified_on?: string; type: string; diff --git a/x-pack/plugins/beats_management/server/lib/domains/beats.ts b/x-pack/plugins/beats_management/server/lib/domains/beats.ts index 49b0ad5543f65..15a14a4c97883 100644 --- a/x-pack/plugins/beats_management/server/lib/domains/beats.ts +++ b/x-pack/plugins/beats_management/server/lib/domains/beats.ts @@ -35,12 +35,18 @@ export class CMBeatsDomain { this.framework = libs.framework; } - public async getById(user: FrameworkUser, beatId: string) { - return await this.adapter.get(user, beatId); + public async getById(user: FrameworkUser, beatId: string): Promise { + const beat = await this.adapter.get(user, beatId); + return beat && beat.active ? beat : null; + } + + public async getAll(user: FrameworkUser) { + return (await this.adapter.getAll(user)).filter((beat: CMBeat) => beat.active === true); } public async getByEnrollmentToken(user: FrameworkUser, enrollmentToken: string) { - return await this.adapter.getBeatWithToken(user, enrollmentToken); + const beat = await this.adapter.getBeatWithToken(user, enrollmentToken); + return beat && beat.active ? beat : null; } public async update(userOrToken: UserOrToken, beatId: string, beatData: Partial) { @@ -143,10 +149,6 @@ export class CMBeatsDomain { return response; } - public async getAllBeats(user: FrameworkUser) { - return await this.adapter.getAll(user); - } - public async assignTagsToBeats( user: FrameworkUser, assignments: BeatsTagAssignment[] diff --git a/x-pack/plugins/beats_management/server/rest_api/beats/list.ts b/x-pack/plugins/beats_management/server/rest_api/beats/list.ts index 72105fc2f5440..f5e07665c119a 100644 --- a/x-pack/plugins/beats_management/server/rest_api/beats/list.ts +++ b/x-pack/plugins/beats_management/server/rest_api/beats/list.ts @@ -14,7 +14,7 @@ export const createListAgentsRoute = (libs: CMServerLibs) => ({ path: '/api/beats/agents', handler: async (request: FrameworkRequest, reply: any) => { try { - const beats = await libs.beats.getAllBeats(request.user); + const beats = await libs.beats.getAll(request.user); reply({ beats }); } catch (err) { // TODO move this to kibana route thing in adapter diff --git a/x-pack/plugins/beats_management/server/utils/index_templates/beats_template.json b/x-pack/plugins/beats_management/server/utils/index_templates/beats_template.json index c77d8f9719680..0442c31fd7c2d 100644 --- a/x-pack/plugins/beats_management/server/utils/index_templates/beats_template.json +++ b/x-pack/plugins/beats_management/server/utils/index_templates/beats_template.json @@ -51,6 +51,9 @@ "id": { "type": "keyword" }, + "active": { + "type": "boolean" + }, "enrollment_token": { "type": "keyword" }, From 8d1030a07c38432e8b864190d9ba7321ae1498f5 Mon Sep 17 00:00:00 2001 From: Matt Apperson Date: Mon, 6 Aug 2018 15:59:34 -0400 Subject: [PATCH 34/43] disenroll now working --- .../lib/adapters/beats/rest_beats_adapter.ts | 3 ++- .../beats_management/public/pages/main/beats.tsx | 14 ++++++++++---- .../beats_management/server/lib/domains/beats.ts | 1 + 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/beats_management/public/lib/adapters/beats/rest_beats_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/beats/rest_beats_adapter.ts index 1cf134b8293a4..30a052a182945 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/beats/rest_beats_adapter.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/beats/rest_beats_adapter.ts @@ -29,7 +29,8 @@ export class RestBeatsAdapter implements CMBeatsAdapter { } public async update(id: string, beatData: Partial): Promise { - return (await this.REST.put<{ success: true }>(`/api/beats/agent/${id}`, beatData)).success; + await this.REST.put<{ success: true }>(`/api/beats/agent/${id}`, beatData); + return true; } public async removeTagsFromBeats(removals: BeatsTagAssignment[]): Promise { diff --git a/x-pack/plugins/beats_management/public/pages/main/beats.tsx b/x-pack/plugins/beats_management/public/pages/main/beats.tsx index 24baf43ba2c83..a7147b47233f7 100644 --- a/x-pack/plugins/beats_management/public/pages/main/beats.tsx +++ b/x-pack/plugins/beats_management/public/pages/main/beats.tsx @@ -78,10 +78,16 @@ export class BeatsPage extends React.PureComponent { - // const selected = this.getSelectedBeats(); - // await this.props.libs.beats.delete(selected); + const selected = this.getSelectedBeats(); + for (const beat of selected) { + await this.props.libs.beats.update(beat.id, { active: false }); + } + // because the compile code above has a very minor race condition, we wait, + // the max race condition time is really 10ms but doing 100 to be safe + setTimeout(async () => { + await this.loadBeats(); + }, 100); }; private async loadBeats() { @@ -142,7 +148,7 @@ export class BeatsPage extends React.PureComponent { + private getSelectedBeats = (): CMPopulatedBeat[] => { return this.state.tableRef.current.state.selection; }; } diff --git a/x-pack/plugins/beats_management/server/lib/domains/beats.ts b/x-pack/plugins/beats_management/server/lib/domains/beats.ts index 15a14a4c97883..56a0cec013709 100644 --- a/x-pack/plugins/beats_management/server/lib/domains/beats.ts +++ b/x-pack/plugins/beats_management/server/lib/domains/beats.ts @@ -95,6 +95,7 @@ export class CMBeatsDomain { await this.adapter.insert(this.framework.internalUser, { ...beat, + active: true, enrollment_token: enrollmentToken, verified_on: verifiedOn, access_token: accessToken, From 013b5cc86a1eb08ab1c215cc3a629dbdf03e56e5 Mon Sep 17 00:00:00 2001 From: Matt Apperson Date: Mon, 6 Aug 2018 16:38:34 -0400 Subject: [PATCH 35/43] fig getAll query --- .../lib/adapters/beats/elasticsearch_beats_adapter.ts | 3 ++- .../lib/domains/__tests__/beats/assign_tags.test.ts | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts index 036d5d5a23e23..c044d5fdf3d87 100644 --- a/x-pack/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts +++ b/x-pack/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts @@ -112,11 +112,12 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { const params = { index: INDEX_NAMES.BEATS, q: 'type:beat', + size: 10000, type: '_doc', }; const response = await this.database.search(user, params); - const beats = _get(response, 'hits.hits', []); + return beats.map((beat: any) => omit(beat._source.beat, ['access_token'])); } diff --git a/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/assign_tags.test.ts b/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/assign_tags.test.ts index 040e48feda7ed..260adecba500d 100644 --- a/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/assign_tags.test.ts +++ b/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/assign_tags.test.ts @@ -39,6 +39,8 @@ describe('Beats Domain Lib', () => { beatsDB = [ { access_token: '9a6c99ae0fd84b068819701169cd8a4b', + active: true, + enrollment_token: '23423423423', host_ip: '1.2.3.4', host_name: 'foo.bar.com', id: 'qux', @@ -46,6 +48,8 @@ describe('Beats Domain Lib', () => { }, { access_token: '188255eb560a4448b72656c5e99cae6f', + active: true, + enrollment_token: 'reertrte', host_ip: '22.33.11.44', host_name: 'baz.bar.com', id: 'baz', @@ -53,6 +57,8 @@ describe('Beats Domain Lib', () => { }, { access_token: '93c4a4dd08564c189a7ec4e4f046b975', + active: true, + enrollment_token: '23s423423423', host_ip: '1.2.3.4', host_name: 'foo.bar.com', id: 'foo', @@ -62,6 +68,8 @@ describe('Beats Domain Lib', () => { }, { access_token: '3c4a4dd08564c189a7ec4e4f046b9759', + enrollment_token: 'gdfsgdf', + active: true, host_ip: '11.22.33.44', host_name: 'foo.com', id: 'bar', From f07e7138c162ee2e86adb8d3c3f46567af021b29 Mon Sep 17 00:00:00 2001 From: Matt Apperson Date: Mon, 6 Aug 2018 17:46:50 -0400 Subject: [PATCH 36/43] re-enrolling a beat will now work --- .../server/lib/adapters/beats/elasticsearch_beats_adapter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts index c044d5fdf3d87..88a7b3cb1cd6b 100644 --- a/x-pack/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts +++ b/x-pack/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts @@ -40,7 +40,7 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { type: 'beat', }; - await this.database.create(user, { + await this.database.index(user, { body, id: `beat:${beat.id}`, index: INDEX_NAMES.BEATS, From c27491e05803eb25989631d9be484383396ea63c Mon Sep 17 00:00:00 2001 From: Matt Apperson Date: Tue, 7 Aug 2018 10:10:26 -0400 Subject: [PATCH 37/43] fix types --- .../beats_management/public/pages/main/beats.tsx | 2 +- .../domains/__tests__/beats/remove_tags.test.ts | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/beats_management/public/pages/main/beats.tsx b/x-pack/plugins/beats_management/public/pages/main/beats.tsx index a7147b47233f7..3ec18bd489c74 100644 --- a/x-pack/plugins/beats_management/public/pages/main/beats.tsx +++ b/x-pack/plugins/beats_management/public/pages/main/beats.tsx @@ -115,7 +115,7 @@ export class BeatsPage extends React.PureComponent this.removeTagsFromBeats(selectedBeats, tag) diff --git a/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/remove_tags.test.ts b/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/remove_tags.test.ts index 490414ae99cd5..446962924e590 100644 --- a/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/remove_tags.test.ts +++ b/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/remove_tags.test.ts @@ -21,7 +21,8 @@ describe('Beats Domain Lib', () => { beatsDB = [ { access_token: '9a6c99ae0fd84b068819701169cd8a4b', - enrollment_token: '1234', + active: true, + enrollment_token: '123kuil;4', host_ip: '1.2.3.4', host_name: 'foo.bar.com', id: 'qux', @@ -29,7 +30,9 @@ describe('Beats Domain Lib', () => { }, { access_token: '188255eb560a4448b72656c5e99cae6f', - enrollment_token: '1234', + active: true, + + enrollment_token: '12fghjyu34', host_ip: '22.33.11.44', host_name: 'baz.bar.com', id: 'baz', @@ -37,7 +40,9 @@ describe('Beats Domain Lib', () => { }, { access_token: '93c4a4dd08564c189a7ec4e4f046b975', - enrollment_token: '1234', + active: true, + + enrollment_token: '12nfhgj34', host_ip: '1.2.3.4', host_name: 'foo.bar.com', id: 'foo', @@ -47,7 +52,9 @@ describe('Beats Domain Lib', () => { }, { access_token: '3c4a4dd08564c189a7ec4e4f046b9759', - enrollment_token: '1234', + active: true, + + enrollment_token: '123sfd4', host_ip: '11.22.33.44', host_name: 'foo.com', id: 'bar', From ab999d04d31f099bc9a98a9675a90b475a2ddfc6 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Tue, 7 Aug 2018 10:21:07 -0400 Subject: [PATCH 38/43] Add create tags view. --- .../public/pages/main/create_tag.tsx | 181 ++++++++++++++++++ .../public/pages/main/index.tsx | 11 ++ x-pack/yarn.lock | 87 ++++++++- yarn.lock | 79 +++++++- 4 files changed, 356 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/beats_management/public/pages/main/create_tag.tsx diff --git a/x-pack/plugins/beats_management/public/pages/main/create_tag.tsx b/x-pack/plugins/beats_management/public/pages/main/create_tag.tsx new file mode 100644 index 0000000000000..ae6088d072b0b --- /dev/null +++ b/x-pack/plugins/beats_management/public/pages/main/create_tag.tsx @@ -0,0 +1,181 @@ +/* + * 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 { + // @ts-ignore + EuiBadge, + EuiButton, + EuiButtonEmpty, + // @ts-ignore + EuiColorPicker, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + // @ts-ignore + EuiForm, + EuiFormRow, + EuiPanel, + // @ts-ignore + EuiSelect, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import 'brace/mode/yaml'; +import 'brace/theme/github'; +import React from 'react'; +import { ConfigurationBlockTypes } from '../../../common/constants'; +import { ConfigurationBlock } from '../../../common/domain_types'; +import { FrontendLibs } from '../../lib/lib'; + +interface CreateTagPageProps { + libs: FrontendLibs; +} + +interface CreateTagPageState { + color: string | null; + configurationBlocks: ConfigurationBlock[]; + tagName: string | null; +} + +export class CreateTagPage extends React.PureComponent { + constructor(props: CreateTagPageProps) { + super(props); + + this.state = { + color: '#DD0A73', + configurationBlocks: [], + tagName: null, + }; + } + + public render() { + const { color, configurationBlocks, tagName } = this.state; + return ( +
+ +

Add a new tag

+
+ + + + + +

Define this tag

+
+ +

+ Tags will apply a set configuration to a group of beats. +
+ The tag type defines the options available. +

+
+
+ {tagName ? tagName : 'Tag name'} +
+
+ + + + + + + + + + +
+
+ + + + + +

Configurations

+
+ +

+ You can have multiple configurations applied to an individual tag. These + configurations can repeat or mix types as necessary. For example, you may utilize + three metricbeat configurations alongside one input and filebeat configuration. +

+
+
+ +
+ {configurationBlocks.length > 0 && ( +
+ {configurationBlocks.map((block, index) => ( +
+ + + + + + + +
+ ))} + +
+ )} + Add a new configuration +
+
+
+
+ + + + + Save + + + + Cancel + + +
+ ); + } + + private getConfigurationOptions = () => { + const types = []; + for (const type in ConfigurationBlockTypes) { + if (typeof ConfigurationBlockTypes[type] === 'string') { + types.push({ text: type, value: ConfigurationBlockTypes[type] }); + } + } + return types; + }; + + private addConfigurationBlock = () => { + const { configurationBlocks } = this.state; + this.setState({ + configurationBlocks: [ + ...configurationBlocks, + { + type: ConfigurationBlockTypes.FilebeatInputs, + block_yml: '', + }, + ], + }); + }; + private handleConfigChange = (index: number, value: string) => { + const { configurationBlocks } = this.state; + configurationBlocks[index].block_yml = value; + this.setState({ configurationBlocks }); + }; + private getOnConfigurationBlockUpdate = (index: number) => (e: any) => { + this.handleConfigChange(index, e.target.value); + }; + private updateBadgeName = (e: any) => this.setState({ tagName: e.target.value }); + private updateBadgeColor = (e: any) => this.setState({ color: e }); +} diff --git a/x-pack/plugins/beats_management/public/pages/main/index.tsx b/x-pack/plugins/beats_management/public/pages/main/index.tsx index 49c4f757079cd..c773d66378e95 100644 --- a/x-pack/plugins/beats_management/public/pages/main/index.tsx +++ b/x-pack/plugins/beats_management/public/pages/main/index.tsx @@ -16,6 +16,7 @@ import { PrimaryLayout } from '../../components/layouts/primary'; import { FrontendLibs } from '../../lib/lib'; import { ActivityPage } from './activity'; import { BeatsPage } from './beats'; +import { CreateTagPage } from './create_tag'; import { TagsPage } from './tags'; interface MainPagesProps { @@ -60,6 +61,11 @@ export class MainPages extends React.PureComponent ( @@ -106,6 +112,11 @@ export class MainPages extends React.PureComponent } /> + } + /> ); diff --git a/x-pack/yarn.lock b/x-pack/yarn.lock index 7d1dc68653e81..fdf775dddc6ab 100644 --- a/x-pack/yarn.lock +++ b/x-pack/yarn.lock @@ -123,10 +123,18 @@ version "4.3.10" resolved "https://registry.yarnpkg.com/@types/boom/-/boom-4.3.10.tgz#39dad8c0614c26b91ef016a57d7eee4ffe4f8a25" +"@types/chance@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/chance/-/chance-1.0.1.tgz#c10703020369602c40dd9428cc6e1437027116df" + "@types/delay@^2.0.1": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/delay/-/delay-2.0.1.tgz#61bcf318a74b61e79d1658fbf054f984c90ef901" +"@types/elasticsearch@^5.0.24": + version "5.0.25" + resolved "https://registry.yarnpkg.com/@types/elasticsearch/-/elasticsearch-5.0.25.tgz#717255a52acd9fa3ba165072d43a242283b1c898" + "@types/events@*": version "1.2.0" resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86" @@ -161,6 +169,12 @@ version "10.6.2" resolved "https://registry.yarnpkg.com/@types/joi/-/joi-10.6.2.tgz#0e7d632fe918c337784e87b16c7cc0098876179a" +"@types/jsonwebtoken@^7.2.7": + version "7.2.8" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-7.2.8.tgz#8d199dab4ddb5bba3234f8311b804d2027af2b3a" + dependencies: + "@types/node" "*" + "@types/lodash@^3.10.0": version "3.10.2" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-3.10.2.tgz#c1fbda1562ef5603c8192fe1fe65b017849d5873" @@ -222,6 +236,10 @@ version "0.10.2" resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.10.2.tgz#bd1740c4ad51966609b058803ee6874577848b37" +"@types/sinon@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-5.0.1.tgz#a15b36ec42f1f53166617491feabd1734cb03e21" + "@types/url-join@^0.8.2": version "0.8.2" resolved "https://registry.yarnpkg.com/@types/url-join/-/url-join-0.8.2.tgz#1181ecbe1d97b7034e0ea1e35e62e86cc26b422d" @@ -1230,6 +1248,10 @@ buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + buffer-equal@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-0.0.1.tgz#91bc74b11ea405bc916bc6aa908faafa5b4aac4b" @@ -2174,6 +2196,12 @@ ecc-jsbn@~0.1.1: dependencies: jsbn "~0.1.0" +ecdsa-sig-formatter@1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.10.tgz#1c595000f04a8897dfb85000892a0f4c33af86c3" + dependencies: + safe-buffer "^5.0.1" + elasticsearch@^14.1.0: version "14.2.2" resolved "https://registry.yarnpkg.com/elasticsearch/-/elasticsearch-14.2.2.tgz#6bbb63b19b17fa97211b22eeacb0f91197f4d6b6" @@ -4502,6 +4530,20 @@ jsonpointer@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" +jsonwebtoken@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.3.0.tgz#056c90eee9a65ed6e6c72ddb0a1d325109aaf643" + dependencies: + jws "^3.1.5" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + jsprim@^1.2.2: version "1.4.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" @@ -4519,6 +4561,21 @@ just-extend@^1.1.27: version "1.1.27" resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-1.1.27.tgz#ec6e79410ff914e472652abfa0e603c03d60e905" +jwa@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.6.tgz#87240e76c9808dbde18783cf2264ef4929ee50e6" + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.10" + safe-buffer "^5.0.1" + +jws@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.5.tgz#80d12d05b293d1e841e7cb8b4e69e561adcf834f" + dependencies: + jwa "^1.1.5" + safe-buffer "^5.0.1" + keymirror@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/keymirror/-/keymirror-0.1.1.tgz#918889ea13f8d0a42e7c557250eee713adc95c35" @@ -4770,6 +4827,10 @@ lodash.get@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + lodash.isarguments@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" @@ -4778,6 +4839,10 @@ lodash.isarray@^3.0.0: version "3.0.4" resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + lodash.isempty@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.isempty/-/lodash.isempty-4.4.0.tgz#6f86cbedd8be4ec987be9aaf33c9684db1b31e7e" @@ -4786,10 +4851,26 @@ lodash.isequal@^4.1.1, lodash.isequal@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + lodash.isobject@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-3.0.2.tgz#3c8fb8d5b5bf4bf90ae06e14f2a530a4ed935e1d" +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + lodash.istypedarray@^3.0.0: version "3.0.6" resolved "https://registry.yarnpkg.com/lodash.istypedarray/-/lodash.istypedarray-3.0.6.tgz#c9a477498607501d8e8494d283b87c39281cef62" @@ -4810,6 +4891,10 @@ lodash.mergewith@^4.6.0: version "4.6.1" resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927" +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + lodash.orderby@4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.orderby/-/lodash.orderby-4.6.0.tgz#e697f04ce5d78522f54d9338b32b81a3393e4eb3" @@ -5227,7 +5312,7 @@ ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" -ms@^2.0.0: +ms@^2.0.0, ms@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" diff --git a/yarn.lock b/yarn.lock index 92bd3108bf025..5fa60901215e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -304,6 +304,10 @@ version "2.0.1" resolved "https://registry.yarnpkg.com/@types/delay/-/delay-2.0.1.tgz#61bcf318a74b61e79d1658fbf054f984c90ef901" +"@types/elasticsearch@^5.0.24": + version "5.0.25" + resolved "https://registry.yarnpkg.com/@types/elasticsearch/-/elasticsearch-5.0.25.tgz#717255a52acd9fa3ba165072d43a242283b1c898" + "@types/enzyme@^3.1.12": version "3.1.12" resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.1.12.tgz#293bb07c1ef5932d37add3879e72e0f5bc614f3c" @@ -417,6 +421,12 @@ version "1.0.32" resolved "https://registry.yarnpkg.com/@types/json-stable-stringify/-/json-stable-stringify-1.0.32.tgz#121f6917c4389db3923640b2e68de5fa64dda88e" +"@types/jsonwebtoken@^7.2.7": + version "7.2.8" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-7.2.8.tgz#8d199dab4ddb5bba3234f8311b804d2027af2b3a" + dependencies: + "@types/node" "*" + "@types/listr@^0.13.0": version "0.13.0" resolved "https://registry.yarnpkg.com/@types/listr/-/listr-0.13.0.tgz#6250bc4a04123cafa24fc73d1b880653a6ae6721" @@ -2282,6 +2292,10 @@ buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + buffer-equal@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-0.0.1.tgz#91bc74b11ea405bc916bc6aa908faafa5b4aac4b" @@ -4210,6 +4224,12 @@ ecc-jsbn@~0.1.1: dependencies: jsbn "~0.1.0" +ecdsa-sig-formatter@1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.10.tgz#1c595000f04a8897dfb85000892a0f4c33af86c3" + dependencies: + safe-buffer "^5.0.1" + editions@^1.1.1, editions@^1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/editions/-/editions-1.3.4.tgz#3662cb592347c3168eb8e498a0ff73271d67f50b" @@ -7833,6 +7853,20 @@ jsonpointer@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" +jsonwebtoken@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.3.0.tgz#056c90eee9a65ed6e6c72ddb0a1d325109aaf643" + dependencies: + jws "^3.1.5" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + jsprim@^1.2.2: version "1.4.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" @@ -7881,6 +7915,21 @@ just-extend@^1.1.27: version "1.1.27" resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-1.1.27.tgz#ec6e79410ff914e472652abfa0e603c03d60e905" +jwa@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.6.tgz#87240e76c9808dbde18783cf2264ef4929ee50e6" + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.10" + safe-buffer "^5.0.1" + +jws@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.5.tgz#80d12d05b293d1e841e7cb8b4e69e561adcf834f" + dependencies: + jwa "^1.1.5" + safe-buffer "^5.0.1" + karma-chrome-launcher@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-2.1.1.tgz#216879c68ac04d8d5140e99619ba04b59afd46cf" @@ -8417,6 +8466,10 @@ lodash.get@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + lodash.isarguments@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" @@ -8425,6 +8478,10 @@ lodash.isarray@^3.0.0: version "3.0.4" resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + lodash.isempty@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.isempty/-/lodash.isempty-4.4.0.tgz#6f86cbedd8be4ec987be9aaf33c9684db1b31e7e" @@ -8433,10 +8490,26 @@ lodash.isequal@^4.0.0, lodash.isequal@^4.1.1, lodash.isequal@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + lodash.isobject@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-3.0.2.tgz#3c8fb8d5b5bf4bf90ae06e14f2a530a4ed935e1d" +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + lodash.istypedarray@^3.0.0: version "3.0.6" resolved "https://registry.yarnpkg.com/lodash.istypedarray/-/lodash.istypedarray-3.0.6.tgz#c9a477498607501d8e8494d283b87c39281cef62" @@ -8473,6 +8546,10 @@ lodash.mergewith@^4.6.0: version "4.6.1" resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927" +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + lodash.orderby@4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.orderby/-/lodash.orderby-4.6.0.tgz#e697f04ce5d78522f54d9338b32b81a3393e4eb3" @@ -9040,7 +9117,7 @@ ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" -ms@^2.0.0: +ms@^2.0.0, ms@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" From f7c01feb7274699f3d150fccef44040d8b9e0249 Mon Sep 17 00:00:00 2001 From: Matt Apperson Date: Tue, 7 Aug 2018 10:24:23 -0400 Subject: [PATCH 39/43] fix types --- .../public/lib/adapters/beats/memory_beats_adapter.ts | 1 + .../server/lib/domains/__tests__/beats/assign_tags.test.ts | 3 +++ .../server/lib/domains/__tests__/beats/remove_tags.test.ts | 3 +++ x-pack/plugins/beats_management/server/lib/domains/tags.ts | 1 + 4 files changed, 8 insertions(+) diff --git a/x-pack/plugins/beats_management/public/lib/adapters/beats/memory_beats_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/beats/memory_beats_adapter.ts index eb497f20af59a..745a11ac65464 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/beats/memory_beats_adapter.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/beats/memory_beats_adapter.ts @@ -51,6 +51,7 @@ export class MemoryBeatsAdapter implements CMBeatsAdapter { if (beat.tags) { beat.tags = beat.tags.filter(tag => tag !== tagData.tag); } + } const removalsForBeat = removals.filter(r => r.beatId === beat.id); if (removalsForBeat.length) { removalsForBeat.forEach((assignment: BeatsTagAssignment) => { diff --git a/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/assign_tags.test.ts b/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/assign_tags.test.ts index 260adecba500d..48b1f63335ec7 100644 --- a/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/assign_tags.test.ts +++ b/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/assign_tags.test.ts @@ -80,14 +80,17 @@ describe('Beats Domain Lib', () => { { configuration_blocks: [], id: 'production', + last_updated: new Date(), }, { configuration_blocks: [], id: 'development', + last_updated: new Date(), }, { configuration_blocks: [], id: 'qa', + last_updated: new Date(), }, ]; const framework = new TestingBackendFrameworkAdapter(settings); diff --git a/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/remove_tags.test.ts b/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/remove_tags.test.ts index e429a4d977535..a35da72889111 100644 --- a/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/remove_tags.test.ts +++ b/x-pack/plugins/beats_management/server/lib/domains/__tests__/beats/remove_tags.test.ts @@ -63,14 +63,17 @@ describe('Beats Domain Lib', () => { { configuration_blocks: [], id: 'production', + last_updated: new Date(), }, { configuration_blocks: [], id: 'development', + last_updated: new Date(), }, { configuration_blocks: [], id: 'qa', + last_updated: new Date(), }, ]; diff --git a/x-pack/plugins/beats_management/server/lib/domains/tags.ts b/x-pack/plugins/beats_management/server/lib/domains/tags.ts index 39bc2c147ea03..5f5e6747cc847 100644 --- a/x-pack/plugins/beats_management/server/lib/domains/tags.ts +++ b/x-pack/plugins/beats_management/server/lib/domains/tags.ts @@ -35,6 +35,7 @@ export class CMTagsDomain { const tag = { configuration_blocks: configs, id: tagId, + last_updated: new Date(), }; return { isValid: true, From f8faae98ad3a4642a9c3751d6be6f6b044008685 Mon Sep 17 00:00:00 2001 From: Matt Apperson Date: Tue, 7 Aug 2018 14:53:23 -0400 Subject: [PATCH 40/43] update deps --- x-pack/yarn.lock | 87 +++++++++++++++++++++++++++++++++++++++++++++++- yarn.lock | 79 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 164 insertions(+), 2 deletions(-) diff --git a/x-pack/yarn.lock b/x-pack/yarn.lock index 7d1dc68653e81..fdf775dddc6ab 100644 --- a/x-pack/yarn.lock +++ b/x-pack/yarn.lock @@ -123,10 +123,18 @@ version "4.3.10" resolved "https://registry.yarnpkg.com/@types/boom/-/boom-4.3.10.tgz#39dad8c0614c26b91ef016a57d7eee4ffe4f8a25" +"@types/chance@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/chance/-/chance-1.0.1.tgz#c10703020369602c40dd9428cc6e1437027116df" + "@types/delay@^2.0.1": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/delay/-/delay-2.0.1.tgz#61bcf318a74b61e79d1658fbf054f984c90ef901" +"@types/elasticsearch@^5.0.24": + version "5.0.25" + resolved "https://registry.yarnpkg.com/@types/elasticsearch/-/elasticsearch-5.0.25.tgz#717255a52acd9fa3ba165072d43a242283b1c898" + "@types/events@*": version "1.2.0" resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86" @@ -161,6 +169,12 @@ version "10.6.2" resolved "https://registry.yarnpkg.com/@types/joi/-/joi-10.6.2.tgz#0e7d632fe918c337784e87b16c7cc0098876179a" +"@types/jsonwebtoken@^7.2.7": + version "7.2.8" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-7.2.8.tgz#8d199dab4ddb5bba3234f8311b804d2027af2b3a" + dependencies: + "@types/node" "*" + "@types/lodash@^3.10.0": version "3.10.2" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-3.10.2.tgz#c1fbda1562ef5603c8192fe1fe65b017849d5873" @@ -222,6 +236,10 @@ version "0.10.2" resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.10.2.tgz#bd1740c4ad51966609b058803ee6874577848b37" +"@types/sinon@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-5.0.1.tgz#a15b36ec42f1f53166617491feabd1734cb03e21" + "@types/url-join@^0.8.2": version "0.8.2" resolved "https://registry.yarnpkg.com/@types/url-join/-/url-join-0.8.2.tgz#1181ecbe1d97b7034e0ea1e35e62e86cc26b422d" @@ -1230,6 +1248,10 @@ buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + buffer-equal@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-0.0.1.tgz#91bc74b11ea405bc916bc6aa908faafa5b4aac4b" @@ -2174,6 +2196,12 @@ ecc-jsbn@~0.1.1: dependencies: jsbn "~0.1.0" +ecdsa-sig-formatter@1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.10.tgz#1c595000f04a8897dfb85000892a0f4c33af86c3" + dependencies: + safe-buffer "^5.0.1" + elasticsearch@^14.1.0: version "14.2.2" resolved "https://registry.yarnpkg.com/elasticsearch/-/elasticsearch-14.2.2.tgz#6bbb63b19b17fa97211b22eeacb0f91197f4d6b6" @@ -4502,6 +4530,20 @@ jsonpointer@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" +jsonwebtoken@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.3.0.tgz#056c90eee9a65ed6e6c72ddb0a1d325109aaf643" + dependencies: + jws "^3.1.5" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + jsprim@^1.2.2: version "1.4.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" @@ -4519,6 +4561,21 @@ just-extend@^1.1.27: version "1.1.27" resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-1.1.27.tgz#ec6e79410ff914e472652abfa0e603c03d60e905" +jwa@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.6.tgz#87240e76c9808dbde18783cf2264ef4929ee50e6" + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.10" + safe-buffer "^5.0.1" + +jws@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.5.tgz#80d12d05b293d1e841e7cb8b4e69e561adcf834f" + dependencies: + jwa "^1.1.5" + safe-buffer "^5.0.1" + keymirror@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/keymirror/-/keymirror-0.1.1.tgz#918889ea13f8d0a42e7c557250eee713adc95c35" @@ -4770,6 +4827,10 @@ lodash.get@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + lodash.isarguments@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" @@ -4778,6 +4839,10 @@ lodash.isarray@^3.0.0: version "3.0.4" resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + lodash.isempty@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.isempty/-/lodash.isempty-4.4.0.tgz#6f86cbedd8be4ec987be9aaf33c9684db1b31e7e" @@ -4786,10 +4851,26 @@ lodash.isequal@^4.1.1, lodash.isequal@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + lodash.isobject@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-3.0.2.tgz#3c8fb8d5b5bf4bf90ae06e14f2a530a4ed935e1d" +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + lodash.istypedarray@^3.0.0: version "3.0.6" resolved "https://registry.yarnpkg.com/lodash.istypedarray/-/lodash.istypedarray-3.0.6.tgz#c9a477498607501d8e8494d283b87c39281cef62" @@ -4810,6 +4891,10 @@ lodash.mergewith@^4.6.0: version "4.6.1" resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927" +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + lodash.orderby@4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.orderby/-/lodash.orderby-4.6.0.tgz#e697f04ce5d78522f54d9338b32b81a3393e4eb3" @@ -5227,7 +5312,7 @@ ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" -ms@^2.0.0: +ms@^2.0.0, ms@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" diff --git a/yarn.lock b/yarn.lock index 92bd3108bf025..5fa60901215e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -304,6 +304,10 @@ version "2.0.1" resolved "https://registry.yarnpkg.com/@types/delay/-/delay-2.0.1.tgz#61bcf318a74b61e79d1658fbf054f984c90ef901" +"@types/elasticsearch@^5.0.24": + version "5.0.25" + resolved "https://registry.yarnpkg.com/@types/elasticsearch/-/elasticsearch-5.0.25.tgz#717255a52acd9fa3ba165072d43a242283b1c898" + "@types/enzyme@^3.1.12": version "3.1.12" resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.1.12.tgz#293bb07c1ef5932d37add3879e72e0f5bc614f3c" @@ -417,6 +421,12 @@ version "1.0.32" resolved "https://registry.yarnpkg.com/@types/json-stable-stringify/-/json-stable-stringify-1.0.32.tgz#121f6917c4389db3923640b2e68de5fa64dda88e" +"@types/jsonwebtoken@^7.2.7": + version "7.2.8" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-7.2.8.tgz#8d199dab4ddb5bba3234f8311b804d2027af2b3a" + dependencies: + "@types/node" "*" + "@types/listr@^0.13.0": version "0.13.0" resolved "https://registry.yarnpkg.com/@types/listr/-/listr-0.13.0.tgz#6250bc4a04123cafa24fc73d1b880653a6ae6721" @@ -2282,6 +2292,10 @@ buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + buffer-equal@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-0.0.1.tgz#91bc74b11ea405bc916bc6aa908faafa5b4aac4b" @@ -4210,6 +4224,12 @@ ecc-jsbn@~0.1.1: dependencies: jsbn "~0.1.0" +ecdsa-sig-formatter@1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.10.tgz#1c595000f04a8897dfb85000892a0f4c33af86c3" + dependencies: + safe-buffer "^5.0.1" + editions@^1.1.1, editions@^1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/editions/-/editions-1.3.4.tgz#3662cb592347c3168eb8e498a0ff73271d67f50b" @@ -7833,6 +7853,20 @@ jsonpointer@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" +jsonwebtoken@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.3.0.tgz#056c90eee9a65ed6e6c72ddb0a1d325109aaf643" + dependencies: + jws "^3.1.5" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + jsprim@^1.2.2: version "1.4.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" @@ -7881,6 +7915,21 @@ just-extend@^1.1.27: version "1.1.27" resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-1.1.27.tgz#ec6e79410ff914e472652abfa0e603c03d60e905" +jwa@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.6.tgz#87240e76c9808dbde18783cf2264ef4929ee50e6" + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.10" + safe-buffer "^5.0.1" + +jws@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.5.tgz#80d12d05b293d1e841e7cb8b4e69e561adcf834f" + dependencies: + jwa "^1.1.5" + safe-buffer "^5.0.1" + karma-chrome-launcher@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-2.1.1.tgz#216879c68ac04d8d5140e99619ba04b59afd46cf" @@ -8417,6 +8466,10 @@ lodash.get@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + lodash.isarguments@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" @@ -8425,6 +8478,10 @@ lodash.isarray@^3.0.0: version "3.0.4" resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + lodash.isempty@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.isempty/-/lodash.isempty-4.4.0.tgz#6f86cbedd8be4ec987be9aaf33c9684db1b31e7e" @@ -8433,10 +8490,26 @@ lodash.isequal@^4.0.0, lodash.isequal@^4.1.1, lodash.isequal@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + lodash.isobject@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-3.0.2.tgz#3c8fb8d5b5bf4bf90ae06e14f2a530a4ed935e1d" +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + lodash.istypedarray@^3.0.0: version "3.0.6" resolved "https://registry.yarnpkg.com/lodash.istypedarray/-/lodash.istypedarray-3.0.6.tgz#c9a477498607501d8e8494d283b87c39281cef62" @@ -8473,6 +8546,10 @@ lodash.mergewith@^4.6.0: version "4.6.1" resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927" +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + lodash.orderby@4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.orderby/-/lodash.orderby-4.6.0.tgz#e697f04ce5d78522f54d9338b32b81a3393e4eb3" @@ -9040,7 +9117,7 @@ ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" -ms@^2.0.0: +ms@^2.0.0, ms@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" From e2707744d9c9b1db41c5654cc3432c0afce4ffbb Mon Sep 17 00:00:00 2001 From: Matt Apperson Date: Tue, 7 Aug 2018 15:26:27 -0400 Subject: [PATCH 41/43] update kibana API for version --- .../lib/adapters/framework/kibana_framework_adapter.ts | 1 - .../lib/adapters/rest_api/axios_rest_api_adapter.ts | 8 ++------ .../plugins/beats_management/public/lib/compose/kibana.ts | 3 +-- .../lib/adapters/beats/elasticsearch_beats_adapter.ts | 5 ----- 4 files changed, 3 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/beats_management/public/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/framework/kibana_framework_adapter.ts index 1a61a5581ce4e..497f47a48d57d 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/framework/kibana_framework_adapter.ts @@ -17,7 +17,6 @@ import { export class KibanaFrameworkAdapter implements FrameworkAdapter { public appState: object; - public kbnVersion?: string; private management: any; private adapterService: KibanaAdapterServiceProvider; diff --git a/x-pack/plugins/beats_management/public/lib/adapters/rest_api/axios_rest_api_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/rest_api/axios_rest_api_adapter.ts index b05f2b41e30b3..56bd9b63df686 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/rest_api/axios_rest_api_adapter.ts +++ b/x-pack/plugins/beats_management/public/lib/adapters/rest_api/axios_rest_api_adapter.ts @@ -9,11 +9,7 @@ import { RestAPIAdapter } from './adapter_types'; let globalAPI: AxiosInstance; export class AxiosRestAPIAdapter implements RestAPIAdapter { - constructor( - private readonly kbnVersion: string, - private readonly xsrfToken: string, - private readonly basePath: string - ) {} + constructor(private readonly xsrfToken: string, private readonly basePath: string) {} public async get(url: string): Promise { return await this.REST.get(url).then(resp => resp.data); @@ -48,7 +44,7 @@ export class AxiosRestAPIAdapter implements RestAPIAdapter { Accept: 'application/json', credentials: 'same-origin', 'Content-Type': 'application/json', - 'kbn-version': this.kbnVersion, + 'kbn-version': this.xsrfToken, 'kbn-xsrf': this.xsrfToken, }, }); diff --git a/x-pack/plugins/beats_management/public/lib/compose/kibana.ts b/x-pack/plugins/beats_management/public/lib/compose/kibana.ts index 9f8f183170fd1..ef395a54ba73b 100644 --- a/x-pack/plugins/beats_management/public/lib/compose/kibana.ts +++ b/x-pack/plugins/beats_management/public/lib/compose/kibana.ts @@ -22,8 +22,7 @@ import { RestTokensAdapter } from '../adapters/tokens/rest_tokens_adapter'; import { FrontendDomainLibs, FrontendLibs } from '../lib'; export function compose(): FrontendLibs { - const kbnVersion = (window as any).__KBN__.version; - const api = new AxiosRestAPIAdapter(kbnVersion, chrome.getXsrfToken(), chrome.getBasePath()); + const api = new AxiosRestAPIAdapter(chrome.getXsrfToken(), chrome.getBasePath()); const tags = new RestTagsAdapter(api); const tokens = new RestTokensAdapter(api); diff --git a/x-pack/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts index 52bed53f2de1d..22bba3661a752 100644 --- a/x-pack/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts +++ b/x-pack/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts @@ -9,7 +9,6 @@ import { INDEX_NAMES } from '../../../../common/constants'; import { CMBeat } from '../../../../common/domain_types'; import { DatabaseAdapter } from '../database/adapter_types'; -import { BackendFrameworkAdapter } from '../framework/adapter_types'; import { FrameworkUser } from '../framework/adapter_types'; import { BeatsTagAssignment, CMBeatsAdapter } from './adapter_types'; @@ -18,7 +17,6 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { constructor(database: DatabaseAdapter) { this.database = database; - } public async get(user: FrameworkUser, id: string) { @@ -38,7 +36,6 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { } public async insert(user: FrameworkUser, beat: CMBeat) { - const body = { beat, type: 'beat', @@ -54,7 +51,6 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { } public async update(user: FrameworkUser, beat: CMBeat) { - const body = { beat, type: 'beat', @@ -141,7 +137,6 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { return [ { update: { _id: `beat:${beatId}` } }, { script: { source: script.replace(' ', ''), params: { tag } } }, - ]; }) ); From 369e2007446532ada109a3bac41d182d1d9a93ea Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Wed, 8 Aug 2018 01:36:37 -0400 Subject: [PATCH 42/43] Added component/config interface for editing/creating tags. Added separate pages for create/edit tags. --- .../public/components/tag/index.ts | 8 + .../public/components/tag/tag_configs.ts | 17 ++ .../public/components/tag/tag_edit.tsx | 232 ++++++++++++++++++ .../public/pages/main/create_tag.tsx | 149 +---------- .../public/pages/main/edit_tag.tsx | 40 +++ .../public/pages/main/index.tsx | 12 + 6 files changed, 313 insertions(+), 145 deletions(-) create mode 100644 x-pack/plugins/beats_management/public/components/tag/index.ts create mode 100644 x-pack/plugins/beats_management/public/components/tag/tag_configs.ts create mode 100644 x-pack/plugins/beats_management/public/components/tag/tag_edit.tsx create mode 100644 x-pack/plugins/beats_management/public/pages/main/edit_tag.tsx diff --git a/x-pack/plugins/beats_management/public/components/tag/index.ts b/x-pack/plugins/beats_management/public/components/tag/index.ts new file mode 100644 index 0000000000000..8447142e16a73 --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/tag/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { TagCreateConfig, TagEditConfig, TagViewConfig } from './tag_configs'; +export { TagEdit } from './tag_edit'; diff --git a/x-pack/plugins/beats_management/public/components/tag/tag_configs.ts b/x-pack/plugins/beats_management/public/components/tag/tag_configs.ts new file mode 100644 index 0000000000000..08ad711e798de --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/tag/tag_configs.ts @@ -0,0 +1,17 @@ +/* + * 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 interface TagViewConfig { + showAttachedBeats: boolean; +} + +export const TagCreateConfig: TagViewConfig = { + showAttachedBeats: false, +}; + +export const TagEditConfig: TagViewConfig = { + showAttachedBeats: true, +}; diff --git a/x-pack/plugins/beats_management/public/components/tag/tag_edit.tsx b/x-pack/plugins/beats_management/public/components/tag/tag_edit.tsx new file mode 100644 index 0000000000000..a85f46218685d --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/tag/tag_edit.tsx @@ -0,0 +1,232 @@ +/* + * 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 { + // @ts-ignore + EuiBadge, + EuiButton, + EuiButtonEmpty, + // @ts-ignore + EuiCodeEditor, + // @ts-ignore + EuiColorPicker, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + // @ts-ignore + EuiForm, + EuiFormRow, + EuiPanel, + // @ts-ignore + EuiSearchBar, + EuiSpacer, + // @ts-ignore + EuiTabbedContent, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import 'brace/mode/yaml'; +import 'brace/theme/github'; +import React from 'react'; +import { ConfigurationBlock } from '../../../common/domain_types'; +import { Table } from '../table'; +import { BeatsTableType } from '../table'; +import { TagViewConfig } from '../tag'; + +interface TagEditProps { + items: any[]; + config: TagViewConfig; +} + +interface TagEditState { + color: string | null; + configurationBlocks: ConfigurationBlock[]; + showFlyout: boolean; + tableRef: any; + tagName: string | null; +} + +export class TagEdit extends React.PureComponent { + constructor(props: TagEditProps) { + super(props); + + this.state = { + color: '#DD0A73', + configurationBlocks: [], + showFlyout: false, + tableRef: React.createRef(), + tagName: null, + }; + } + + public render() { + const { + config: { showAttachedBeats }, + items, + } = this.props; + const { color, configurationBlocks, tagName } = this.state; + return ( +
+ +

Add a new tag

+
+ + + + + +

Define this tag

+
+ +

+ Tags will apply a set configuration to a group of beats. +
+ The tag type defines the options available. +

+
+
+ {tagName ? tagName : 'Tag name'} +
+
+ + + + + + + + + + +
+
+ + + + + +

Configurations

+
+ +

+ You can have multiple configurations applied to an individual tag. These + configurations can repeat or mix types as necessary. For example, you may utilize + three metricbeat configurations alongside one input and filebeat configuration. +

+
+
+ +
+ Add a new configuration +
+
+
+
+ + {showAttachedBeats && ( + + +

Attached Beats

+
+
{ + /* TODO: handle assignment/delete actions */ + }} + assignmentOptions={[]} + assignmentTitle={null} + items={items} + ref={this.state.tableRef} + showAssignmentOptions={false} + type={BeatsTableType} + /> + + )} + + + + + Save + + + + Cancel + + + {this.state.showFlyout && ( + this.setState({ showFlyout: false })}> + + +

Add Configuration

+
+
+ + + { + // TODO: handle search changes + }} + /> + + + { + // TODO: update field value + }} + placeholder="Description (optional)" + /> + + Add configuration options here, + }, + { + id: 'yaml_editor', + name: 'YAML Editor', + content: , + }, + ]} + /> + + + + + this.setState({ showFlyout: false })} + > + Close + + + + Save + + + +
+ )} + + ); + } + + private openConfigFlyout = () => { + this.setState({ + showFlyout: true, + }); + }; + private updateBadgeColor = (e: any) => this.setState({ color: e }); + private updateBadgeName = (e: any) => this.setState({ tagName: e.target.value }); +} diff --git a/x-pack/plugins/beats_management/public/pages/main/create_tag.tsx b/x-pack/plugins/beats_management/public/pages/main/create_tag.tsx index ae6088d072b0b..27ba69e435eef 100644 --- a/x-pack/plugins/beats_management/public/pages/main/create_tag.tsx +++ b/x-pack/plugins/beats_management/public/pages/main/create_tag.tsx @@ -4,31 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - // @ts-ignore - EuiBadge, - EuiButton, - EuiButtonEmpty, - // @ts-ignore - EuiColorPicker, - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - // @ts-ignore - EuiForm, - EuiFormRow, - EuiPanel, - // @ts-ignore - EuiSelect, - EuiSpacer, - EuiText, - EuiTitle, -} from '@elastic/eui'; import 'brace/mode/yaml'; import 'brace/theme/github'; import React from 'react'; -import { ConfigurationBlockTypes } from '../../../common/constants'; import { ConfigurationBlock } from '../../../common/domain_types'; +import { TagCreateConfig, TagEdit } from '../../components/tag'; import { FrontendLibs } from '../../lib/lib'; interface CreateTagPageProps { @@ -38,6 +18,7 @@ interface CreateTagPageProps { interface CreateTagPageState { color: string | null; configurationBlocks: ConfigurationBlock[]; + showFlyout: boolean; tagName: string | null; } @@ -48,134 +29,12 @@ export class CreateTagPage extends React.PureComponent - -

Add a new tag

-
- - - - - -

Define this tag

-
- -

- Tags will apply a set configuration to a group of beats. -
- The tag type defines the options available. -

-
-
- {tagName ? tagName : 'Tag name'} -
-
- - - - - - - - - - -
-
- - - - - -

Configurations

-
- -

- You can have multiple configurations applied to an individual tag. These - configurations can repeat or mix types as necessary. For example, you may utilize - three metricbeat configurations alongside one input and filebeat configuration. -

-
-
- -
- {configurationBlocks.length > 0 && ( -
- {configurationBlocks.map((block, index) => ( -
- - - - - - - -
- ))} - -
- )} - Add a new configuration -
-
-
-
- - - - - Save - - - - Cancel - - - - ); + return ; } - - private getConfigurationOptions = () => { - const types = []; - for (const type in ConfigurationBlockTypes) { - if (typeof ConfigurationBlockTypes[type] === 'string') { - types.push({ text: type, value: ConfigurationBlockTypes[type] }); - } - } - return types; - }; - - private addConfigurationBlock = () => { - const { configurationBlocks } = this.state; - this.setState({ - configurationBlocks: [ - ...configurationBlocks, - { - type: ConfigurationBlockTypes.FilebeatInputs, - block_yml: '', - }, - ], - }); - }; - private handleConfigChange = (index: number, value: string) => { - const { configurationBlocks } = this.state; - configurationBlocks[index].block_yml = value; - this.setState({ configurationBlocks }); - }; - private getOnConfigurationBlockUpdate = (index: number) => (e: any) => { - this.handleConfigChange(index, e.target.value); - }; - private updateBadgeName = (e: any) => this.setState({ tagName: e.target.value }); - private updateBadgeColor = (e: any) => this.setState({ color: e }); } diff --git a/x-pack/plugins/beats_management/public/pages/main/edit_tag.tsx b/x-pack/plugins/beats_management/public/pages/main/edit_tag.tsx new file mode 100644 index 0000000000000..dab4d82e36844 --- /dev/null +++ b/x-pack/plugins/beats_management/public/pages/main/edit_tag.tsx @@ -0,0 +1,40 @@ +/* + * 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 'brace/mode/yaml'; +import 'brace/theme/github'; +import React from 'react'; +import { ConfigurationBlock } from '../../../common/domain_types'; +import { TagEdit, TagEditConfig } from '../../components/tag'; +import { FrontendLibs } from '../../lib/lib'; + +interface EditTagPageProps { + libs: FrontendLibs; +} + +interface EditTagPageState { + color: string | null; + configurationBlocks: ConfigurationBlock[]; + showFlyout: boolean; + tagName: string | null; +} + +export class EditTagPage extends React.PureComponent { + constructor(props: EditTagPageProps) { + super(props); + + this.state = { + color: '#DD0A73', + configurationBlocks: [], + showFlyout: false, + tagName: null, + }; + } + + public render() { + return ; + } +} diff --git a/x-pack/plugins/beats_management/public/pages/main/index.tsx b/x-pack/plugins/beats_management/public/pages/main/index.tsx index c773d66378e95..0567dac07d787 100644 --- a/x-pack/plugins/beats_management/public/pages/main/index.tsx +++ b/x-pack/plugins/beats_management/public/pages/main/index.tsx @@ -17,6 +17,7 @@ import { FrontendLibs } from '../../lib/lib'; import { ActivityPage } from './activity'; import { BeatsPage } from './beats'; import { CreateTagPage } from './create_tag'; +import { EditTagPage } from './edit_tag'; import { TagsPage } from './tags'; interface MainPagesProps { @@ -66,6 +67,11 @@ export class MainPages extends React.PureComponent ( @@ -117,6 +123,12 @@ export class MainPages extends React.PureComponent } /> + } + /> + hi hi hi ); From 2e484e5de0b2aa61e078d12fb065e1ba4040e3f0 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Wed, 8 Aug 2018 02:56:25 -0400 Subject: [PATCH 43/43] Fixup. --- x-pack/plugins/beats_management/public/pages/main/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugins/beats_management/public/pages/main/index.tsx b/x-pack/plugins/beats_management/public/pages/main/index.tsx index 0567dac07d787..7d201bde445be 100644 --- a/x-pack/plugins/beats_management/public/pages/main/index.tsx +++ b/x-pack/plugins/beats_management/public/pages/main/index.tsx @@ -128,7 +128,6 @@ export class MainPages extends React.PureComponent } /> - hi hi hi );