From cd8f2748ba42fe68bbd52e6b3cd11c6143ad1fd1 Mon Sep 17 00:00:00 2001 From: Matt Apperson Date: Wed, 15 Aug 2018 12:43:23 -0400 Subject: [PATCH] Beats/beat tags workflow (#21923) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Beats Management] Move to Ingest UI arch and initial TS effort (#20039) * [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 * add details page, re-configure routes * move tag crud to new route stuff * update tag create/edit component api * tags creation now working * bunch of stuff I should have split up better… * fixed perf bug, selected items that are removed are no longer phantom selected * fix rendering of assignments * remove assign to beats, the UX was too poor --- x-pack/package.json | 2 + .../public/components/connected_link.tsx | 38 +++ .../public/components/layouts/primary.tsx | 66 +++-- .../public/components/table/action_button.tsx | 1 + .../components/table/assignment_options.tsx | 78 +++--- .../public/components/table/controls.tsx | 4 + .../public/components/table/table.tsx | 13 +- .../components/table/table_type_configs.tsx | 11 +- .../public/components/tag/index.ts | 1 - .../public/components/tag/tag_configs.ts | 17 -- .../public/components/tag/tag_edit.tsx | 63 ++--- .../public/lib/adapters/tags/adapter_types.ts | 1 + .../lib/adapters/tags/memory_tags_adapter.ts | 5 + .../lib/adapters/tags/rest_tags_adapter.ts | 17 +- .../public/lib/compose/kibana.ts | 6 +- .../public/lib/domains/beats.ts | 65 +++++ .../beats_management/public/lib/lib.ts | 4 +- .../public/pages/beat/index.tsx | 18 ++ .../public/pages/main/beats.tsx | 73 ++--- .../public/pages/main/beats_action_area.tsx | 6 +- .../public/pages/main/create_tag.tsx | 40 --- .../public/pages/main/index.tsx | 52 +--- .../public/pages/main/tags.tsx | 53 +++- .../public/pages/tag/create.tsx | 79 ++++++ .../pages/{main/edit_tag.tsx => tag/edit.tsx} | 28 +- .../public/pages/tag/index.tsx | 58 ++++ .../beats_management/public/router.tsx | 18 +- .../lib/adapters/beats/adapter_types.ts | 1 + .../beats/elasticsearch_beats_adapter.ts | 24 +- .../adapters/beats/memory_beats_adapter.ts | 6 +- .../server/lib/adapters/tags/adapter_types.ts | 1 + .../tags/elasticsearch_tags_adapter.ts | 66 ++++- .../lib/adapters/tags/memory_tags_adapter.ts | 4 + .../server/lib/domains/beats.ts | 5 + .../server/lib/domains/tags.ts | 21 +- .../server/management_server.ts | 2 + .../server/rest_api/beats/tag_removal.ts | 10 +- .../server/rest_api/tags/delete.ts | 26 ++ .../server/rest_api/tags/list.ts | 2 +- .../server/rest_api/tags/set.ts | 13 +- .../beats/elasticsearch_beats_adapter.ts | 218 +++++++++++++++ .../kibana/kibana_framework_adapter.ts | 82 ++++++ .../tags/elasticsearch_tags_adapter.ts | 57 ++++ .../tokens/elasticsearch_tokens_adapter.ts | 83 ++++++ .../server/utils/compose/kibana.ts | 45 +++ .../server/utils/domains/beats.ts | 259 ++++++++++++++++++ .../server/utils/domains/tags.ts | 90 ++++++ .../server/utils/domains/tokens.ts | 80 ++++++ .../utils/index_templates/beats_template.json | 3 + .../beats_management/server/utils/lib.ts | 212 ++++++++++++++ .../grokdebugger/common/constants/index.js | 9 - .../call_with_request_factory.js | 18 -- .../common/constants/configuration_blocks.ts | 15 + .../plugins/logstash/server/kibana.index.ts | 14 + .../logstash/server/management_server.ts | 30 ++ .../logstash/server/rest_api/beats/enroll.ts | 63 +++++ .../logstash/server/rest_api/beats/list.ts | 23 ++ .../server/rest_api/beats/tag_assignment.ts | 48 ++++ .../server/rest_api/beats/tag_removal.ts | 48 ++++ .../logstash/server/rest_api/beats/update.ts | 62 +++++ .../logstash/server/rest_api/beats/verify.ts | 73 +++++ .../logstash/server/rest_api/tags/set.ts | 57 ++++ .../logstash/server/rest_api/tokens/create.ts | 42 +++ .../plugins/logstash/server/utils/README.md | 1 + .../server/utils/find_non_existent_items.ts | 14 + .../utils/index_templates/index.ts} | 5 +- .../logstash/server/utils/polyfills.ts | 17 ++ .../logstash/server/utils/wrap_request.ts | 24 ++ x-pack/plugins/logstash/tsconfig.json | 3 + x-pack/plugins/logstash/wallaby.js | 27 ++ .../driver/screenshot_stitcher/index.test.ts | 2 +- .../apis/beats/remove_tags_from_beats.js | 98 +++---- x-pack/yarn.lock | 7 + 73 files changed, 2433 insertions(+), 394 deletions(-) create mode 100644 x-pack/plugins/beats_management/public/components/connected_link.tsx delete mode 100644 x-pack/plugins/beats_management/public/components/tag/tag_configs.ts create mode 100644 x-pack/plugins/beats_management/public/lib/domains/beats.ts create mode 100644 x-pack/plugins/beats_management/public/pages/beat/index.tsx delete mode 100644 x-pack/plugins/beats_management/public/pages/main/create_tag.tsx create mode 100644 x-pack/plugins/beats_management/public/pages/tag/create.tsx rename x-pack/plugins/beats_management/public/pages/{main/edit_tag.tsx => tag/edit.tsx} (58%) create mode 100644 x-pack/plugins/beats_management/public/pages/tag/index.tsx create mode 100644 x-pack/plugins/beats_management/server/rest_api/tags/delete.ts create mode 100644 x-pack/plugins/beats_management/server/utils/adapters/beats/elasticsearch_beats_adapter.ts create mode 100644 x-pack/plugins/beats_management/server/utils/adapters/famework/kibana/kibana_framework_adapter.ts create mode 100644 x-pack/plugins/beats_management/server/utils/adapters/tags/elasticsearch_tags_adapter.ts create mode 100644 x-pack/plugins/beats_management/server/utils/adapters/tokens/elasticsearch_tokens_adapter.ts create mode 100644 x-pack/plugins/beats_management/server/utils/compose/kibana.ts create mode 100644 x-pack/plugins/beats_management/server/utils/domains/beats.ts create mode 100644 x-pack/plugins/beats_management/server/utils/domains/tags.ts create mode 100644 x-pack/plugins/beats_management/server/utils/domains/tokens.ts create mode 100644 x-pack/plugins/beats_management/server/utils/lib.ts delete mode 100644 x-pack/plugins/grokdebugger/common/constants/index.js delete mode 100644 x-pack/plugins/index_management/server/lib/call_with_request_factory/call_with_request_factory.js create mode 100644 x-pack/plugins/logstash/common/constants/configuration_blocks.ts create mode 100644 x-pack/plugins/logstash/server/kibana.index.ts create mode 100644 x-pack/plugins/logstash/server/management_server.ts create mode 100644 x-pack/plugins/logstash/server/rest_api/beats/enroll.ts create mode 100644 x-pack/plugins/logstash/server/rest_api/beats/list.ts create mode 100644 x-pack/plugins/logstash/server/rest_api/beats/tag_assignment.ts create mode 100644 x-pack/plugins/logstash/server/rest_api/beats/tag_removal.ts create mode 100644 x-pack/plugins/logstash/server/rest_api/beats/update.ts create mode 100644 x-pack/plugins/logstash/server/rest_api/beats/verify.ts create mode 100644 x-pack/plugins/logstash/server/rest_api/tags/set.ts create mode 100644 x-pack/plugins/logstash/server/rest_api/tokens/create.ts create mode 100644 x-pack/plugins/logstash/server/utils/README.md create mode 100644 x-pack/plugins/logstash/server/utils/find_non_existent_items.ts rename x-pack/plugins/logstash/{common/constants/index_names.js => server/utils/index_templates/index.ts} (73%) create mode 100644 x-pack/plugins/logstash/server/utils/polyfills.ts create mode 100644 x-pack/plugins/logstash/server/utils/wrap_request.ts create mode 100644 x-pack/plugins/logstash/tsconfig.json create mode 100644 x-pack/plugins/logstash/wallaby.js diff --git a/x-pack/package.json b/x-pack/package.json index 01d5e7cdfedb3..d550731832737 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -30,10 +30,12 @@ "@types/boom": "^4.3.8", "@types/chance": "^1.0.1", "@types/history": "^4.6.2", + "@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/react-router": "^4.0.30", "@types/react-router-dom": "^4.2.7", "@types/sinon": "^5.0.1", "abab": "^1.0.4", diff --git a/x-pack/plugins/beats_management/public/components/connected_link.tsx b/x-pack/plugins/beats_management/public/components/connected_link.tsx new file mode 100644 index 0000000000000..c4b26b0ad93af --- /dev/null +++ b/x-pack/plugins/beats_management/public/components/connected_link.tsx @@ -0,0 +1,38 @@ +/* + * 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 { EuiLink } from '@elastic/eui'; +import { Link, withRouter } from 'react-router-dom'; + +export function ConnectedLinkComponent({ + location, + path, + disabled, + ...props +}: { + location: any; + path: string; + disabled: boolean; + [key: string]: any; +}) { + if (disabled) { + return ; + } + + // Shorthand for pathname + const pathname = path || _.get(props.to, 'pathname') || location.pathname; + + return ( + + ); +} + +export const ConnectedLink = withRouter(ConnectedLinkComponent); diff --git a/x-pack/plugins/beats_management/public/components/layouts/primary.tsx b/x-pack/plugins/beats_management/public/components/layouts/primary.tsx index cd9e8076dd092..71a329c89f955 100644 --- a/x-pack/plugins/beats_management/public/components/layouts/primary.tsx +++ b/x-pack/plugins/beats_management/public/components/layouts/primary.tsx @@ -5,13 +5,24 @@ */ import React from 'react'; +import { withRouter } from 'react-router-dom'; import styled from 'styled-components'; -import { EuiPage, EuiPageBody, EuiPageContent, EuiPageContentBody, EuiTitle } from '@elastic/eui'; +import { + EuiModal, + EuiOverlayMask, + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiTitle, +} from '@elastic/eui'; interface PrimaryLayoutProps { title: string; - actionSection: React.ReactNode; + actionSection?: React.ReactNode; + modalRender?: () => React.ReactNode; + modalClosePath?: string; } const HeaderContainer = styled.div` @@ -22,22 +33,35 @@ const HeaderContainer = styled.div` margin-bottom: 16px; `; -export const PrimaryLayout: React.SFC = ({ - actionSection, - title, - children, -}) => ( - - - - - -

{title}

-
- {actionSection} -
- {children} -
-
-
-); +export const PrimaryLayout: React.SFC = withRouter( + ({ actionSection, title, modalRender, modalClosePath, children, history }) => { + const modalContent = modalRender && modalRender(); + return ( + + + + + +

{title}

+
+ {actionSection} +
+ {children} +
+
+ {modalContent && ( + + { + history.push(modalClosePath); + }} + style={{ width: '640px' }} + > + {modalContent} + + + )} +
+ ); + } +) as any; 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 index e91e620ed8d8c..1f9d2141cba6a 100644 --- a/x-pack/plugins/beats_management/public/components/table/action_button.tsx +++ b/x-pack/plugins/beats_management/public/components/table/action_button.tsx @@ -18,6 +18,7 @@ interface ActionButtonProps { export function ActionButton(props: ActionButtonProps) { const { actions, actionHandler, hidePopover, isPopoverVisible, showPopover } = props; + if (actions.length === 0) { return null; } else if (actions.length === 1) { 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 index 60a6bf2b46952..89affe7ac2a72 100644 --- a/x-pack/plugins/beats_management/public/components/table/assignment_options.tsx +++ b/x-pack/plugins/beats_management/public/components/table/assignment_options.tsx @@ -12,6 +12,7 @@ import { ControlDefinitions } from './table_type_configs'; interface AssignmentOptionsProps { assignmentOptions: any[] | null; assignmentTitle: string | null; + renderAssignmentOptions?: (item: any) => any; controlDefinitions: ControlDefinitions; selectionCount: number; actionHandler(action: string, payload?: any): void; @@ -39,6 +40,7 @@ export class AssignmentOptions extends React.Component< const { actionHandler, assignmentOptions, + renderAssignmentOptions, assignmentTitle, controlDefinitions: { actions }, selectionCount, @@ -60,43 +62,45 @@ export class AssignmentOptions extends React.Component< }} /> - - { - 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 -
- )} -
-
+ {assignmentTitle && ( + + { + this.setState({ + isAssignmentPopoverVisible: true, + }); + actionHandler('loadAssignmentOptions'); + }} + > + {assignmentTitle} + + } + closePopover={() => { + this.setState({ isAssignmentPopoverVisible: false }); + }} + id="assignmentList" + isOpen={isAssignmentPopoverVisible} + panelPaddingSize="s" + withTitle + > + {assignmentOptions && renderAssignmentOptions ? ( + // @ts-ignore direction prop not available on current typing + + {assignmentOptions.map(options => renderAssignmentOptions(options))} + + ) : ( +
+ 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 index f4d85d57d4cf8..07de3cddadfdd 100644 --- a/x-pack/plugins/beats_management/public/components/table/controls.tsx +++ b/x-pack/plugins/beats_management/public/components/table/controls.tsx @@ -15,6 +15,8 @@ import { ControlDefinitions } from './table_type_configs'; interface ControlBarProps { assignmentOptions: any[] | null; assignmentTitle: string | null; + renderAssignmentOptions?: (item: any) => any; + showAssignmentOptions: boolean; controlDefinitions: ControlDefinitions; selectionCount: number; @@ -25,6 +27,7 @@ export function ControlBar(props: ControlBarProps) { const { actionHandler, assignmentOptions, + renderAssignmentOptions, assignmentTitle, controlDefinitions, selectionCount, @@ -36,6 +39,7 @@ export function ControlBar(props: ControlBarProps) { any; items: any[]; showAssignmentOptions: boolean; type: TableType; @@ -26,7 +26,7 @@ interface BeatsTableProps { } interface BeatsTableState { - selection: CMPopulatedBeat[]; + selection: any[]; } const TableContainer = styled.div` @@ -42,11 +42,15 @@ export class Table extends React.Component { }; } - public render() { + public resetSelection = () => { + this.setSelection([]); + }; + public render() { const { actionHandler, assignmentOptions, + renderAssignmentOptions, assignmentTitle, items, showAssignmentOptions, @@ -70,6 +74,7 @@ export class Table extends React.Component { actionHandler(action, payload)} assignmentOptions={assignmentOptions} + renderAssignmentOptions={renderAssignmentOptions} assignmentTitle={assignmentTitle} controlDefinitions={type.controlDefinitions(items)} selectionCount={this.state.selection.length} @@ -89,7 +94,7 @@ export class Table extends React.Component { ); } - private setSelection = (selection: any) => { + 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 index 643c7ce4b810f..e9111c994a337 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 @@ -12,6 +12,7 @@ import React from 'react'; import { TABLE_CONFIG } from '../../../common/constants'; import { BeatTag, CMPopulatedBeat, ConfigurationBlock } from '../../../common/domain_types'; +import { ConnectedLink } from '../connected_link'; export interface ColumnDefinition { align?: string; @@ -55,7 +56,7 @@ export const BeatsTableType: TableType = { { field: 'id', name: 'Beat name', - render: (id: string) => {id}, + render: (id: string) => {id}, sortable: true, }, { @@ -154,7 +155,13 @@ export const TagsTableType: TableType = { }, ], controlDefinitions: (data: any) => ({ - actions: [], + actions: [ + { + name: 'Remove Selected', + action: 'delete', + danger: true, + }, + ], filters: [], }), }; diff --git a/x-pack/plugins/beats_management/public/components/tag/index.ts b/x-pack/plugins/beats_management/public/components/tag/index.ts index 8447142e16a73..bfea59263fc44 100644 --- a/x-pack/plugins/beats_management/public/components/tag/index.ts +++ b/x-pack/plugins/beats_management/public/components/tag/index.ts @@ -4,5 +4,4 @@ * 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 deleted file mode 100644 index 08ad711e798de..0000000000000 --- a/x-pack/plugins/beats_management/public/components/tag/tag_configs.ts +++ /dev/null @@ -1,17 +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 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 index a85f46218685d..2167d42b093ab 100644 --- a/x-pack/plugins/beats_management/public/components/tag/tag_edit.tsx +++ b/x-pack/plugins/beats_management/public/components/tag/tag_edit.tsx @@ -35,22 +35,19 @@ import { import 'brace/mode/yaml'; import 'brace/theme/github'; import React from 'react'; -import { ConfigurationBlock } from '../../../common/domain_types'; +import { BeatTag, CMBeat } from '../../../common/domain_types'; import { Table } from '../table'; import { BeatsTableType } from '../table'; -import { TagViewConfig } from '../tag'; interface TagEditProps { - items: any[]; - config: TagViewConfig; + tag: Partial; + onTagChange: (field: keyof BeatTag, value: string) => any; + attachedBeats: CMBeat[] | null; } interface TagEditState { - color: string | null; - configurationBlocks: ConfigurationBlock[]; showFlyout: boolean; tableRef: any; - tagName: string | null; } export class TagEdit extends React.PureComponent { @@ -58,26 +55,15 @@ export class TagEdit extends React.PureComponent { 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; + const { tag, attachedBeats } = this.props; return (
- -

Add a new tag

-
- @@ -92,20 +78,23 @@ export class TagEdit extends React.PureComponent {

- {tagName ? tagName : 'Tag name'} + + {tag.id ? tag.id : 'Tag name'} +
- + @@ -113,7 +102,11 @@ export class TagEdit extends React.PureComponent {
- +

Configurations

@@ -134,7 +127,7 @@ export class TagEdit extends React.PureComponent {
- {showAttachedBeats && ( + {attachedBeats && (

Attached Beats

@@ -145,24 +138,14 @@ export class TagEdit extends React.PureComponent { }} assignmentOptions={[]} assignmentTitle={null} - items={items} + items={attachedBeats} ref={this.state.tableRef} showAssignmentOptions={false} type={BeatsTableType} />
)} - - - - - Save - - - - Cancel - - + {this.state.showFlyout && ( this.setState({ showFlyout: false })}> @@ -227,6 +210,6 @@ export class TagEdit extends React.PureComponent { showFlyout: true, }); }; - private updateBadgeColor = (e: any) => this.setState({ color: e }); - private updateBadgeName = (e: any) => this.setState({ tagName: e.target.value }); + private updateTag = (key: keyof BeatTag) => (e: any) => + this.props.onTagChange(key, e.target ? e.target.value : e); } 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 57bdf2592baa3..395c01f259dc3 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,6 +7,7 @@ import { BeatTag } from '../../../../common/domain_types'; export interface CMTagsAdapter { getTagsWithIds(tagIds: string[]): Promise; + delete(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 6b7ddd1c98e8c..86daefb47c653 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,11 @@ export class MemoryTagsAdapter implements CMTagsAdapter { return this.tagsDB.filter(tag => tagIds.includes(tag.id)); } + public async delete(tagIds: string[]) { + this.tagsDB = this.tagsDB.filter(tag => !tagIds.includes(tag.id)); + return true; + } + public async getAll() { return this.tagsDB; } 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 fc4a5a157ed71..e49d4a9109984 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,25 @@ export class RestTagsAdapter implements CMTagsAdapter { constructor(private readonly REST: RestAPIAdapter) {} public async getTagsWithIds(tagIds: string[]): Promise { - return (await this.REST.get<{ tags: BeatTag[] }>(`/api/beats/tags/${tagIds.join(',')}`)).tags; + const tags = await this.REST.get(`/api/beats/tags/${tagIds.join(',')}`); + return tags; } public async getAll(): Promise { - return (await this.REST.get<{ tags: BeatTag[] }>(`/api/beats/tags`)).tags; + return await this.REST.get(`/api/beats/tags`); + } + + public async delete(tagIds: string[]): Promise { + return (await this.REST.delete<{ success: boolean }>(`/api/beats/tags/${tagIds.join(',')}`)) + .success; } public async upsertTag(tag: BeatTag): Promise { - return (await this.REST.put<{ tag: BeatTag }>(`/api/beats/tag/{tag}`, { + const response = await this.REST.put<{ success: boolean }>(`/api/beats/tag/${tag.id}`, { + color: tag.color, configuration_blocks: tag.configuration_blocks, - })).tag; + }); + + return response.success ? tag : null; } } 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 ef395a54ba73b..3c445ea8aaee5 100644 --- a/x-pack/plugins/beats_management/public/lib/compose/kibana.ts +++ b/x-pack/plugins/beats_management/public/lib/compose/kibana.ts @@ -13,20 +13,22 @@ import { management } from 'ui/management'; import { uiModules } from 'ui/modules'; // @ts-ignore: path dynamic for kibana import routes from 'ui/routes'; -// @ts-ignore: path dynamic for kibana import { RestBeatsAdapter } from '../adapters/beats/rest_beats_adapter'; import { KibanaFrameworkAdapter } from '../adapters/framework/kibana_framework_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'; +import { BeatsLib } from './../domains/beats'; export function compose(): FrontendLibs { const api = new AxiosRestAPIAdapter(chrome.getXsrfToken(), chrome.getBasePath()); const tags = new RestTagsAdapter(api); const tokens = new RestTokensAdapter(api); - const beats = new RestBeatsAdapter(api); + const beats = new BeatsLib(new RestBeatsAdapter(api), { + tags, + }); const domainLibs: FrontendDomainLibs = { tags, diff --git a/x-pack/plugins/beats_management/public/lib/domains/beats.ts b/x-pack/plugins/beats_management/public/lib/domains/beats.ts new file mode 100644 index 0000000000000..d507cc764de83 --- /dev/null +++ b/x-pack/plugins/beats_management/public/lib/domains/beats.ts @@ -0,0 +1,65 @@ +/* + * 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 } from 'lodash'; +import { CMBeat, CMPopulatedBeat } from '../../../common/domain_types'; +import { + BeatsRemovalReturn, + BeatsTagAssignment, + CMAssignmentReturn, + CMBeatsAdapter, +} from '../adapters/beats/adapter_types'; +import { CMTagsAdapter } from './../adapters/tags/adapter_types'; + +export class BeatsLib { + constructor( + private readonly adapter: CMBeatsAdapter, + private readonly libs: { tags: CMTagsAdapter } + ) {} + + public async get(id: string): Promise { + const beat = await this.adapter.get(id); + return beat ? (await this.mergeInTags([beat]))[0] : null; + } + + public async getBeatWithToken(enrollmentToken: string): Promise { + const beat = await this.adapter.getBeatWithToken(enrollmentToken); + return beat; + } + + public async getAll(): Promise { + const beats = await this.adapter.getAll(); + return await this.mergeInTags(beats); + } + + public async update(id: string, beatData: Partial): Promise { + return await this.adapter.update(id, beatData); + } + + public async removeTagsFromBeats(removals: BeatsTagAssignment[]): Promise { + return await this.adapter.removeTagsFromBeats(removals); + } + + public async assignTagsToBeats(assignments: BeatsTagAssignment[]): Promise { + return await this.adapter.assignTagsToBeats(assignments); + } + + private async mergeInTags(beats: CMBeat[]): Promise { + const tagIds = flatten(beats.map(b => b.tags || [])); + const tags = await this.libs.tags.getTagsWithIds(tagIds); + + // TODO the filter should not be needed, if the data gets into a bad state, we should error + // and inform the user they need to delte the tag, or else we should auto delete it + const mergedBeats: CMPopulatedBeat[] = beats.map( + b => + ({ + ...b, + full_tags: (b.tags || []).map(tagId => tags.find(t => t.id === tagId)).filter(t => t), + } as CMPopulatedBeat) + ); + return mergedBeats; + } +} diff --git a/x-pack/plugins/beats_management/public/lib/lib.ts b/x-pack/plugins/beats_management/public/lib/lib.ts index 03dc74122abcb..c9b3c8f1b4092 100644 --- a/x-pack/plugins/beats_management/public/lib/lib.ts +++ b/x-pack/plugins/beats_management/public/lib/lib.ts @@ -7,12 +7,12 @@ 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'; +import { BeatsLib } from './domains/beats'; export interface FrontendDomainLibs { - beats: CMBeatsAdapter; + beats: BeatsLib; tags: CMTagsAdapter; tokens: CMTokensAdapter; } diff --git a/x-pack/plugins/beats_management/public/pages/beat/index.tsx b/x-pack/plugins/beats_management/public/pages/beat/index.tsx new file mode 100644 index 0000000000000..ace411b47b13b --- /dev/null +++ b/x-pack/plugins/beats_management/public/pages/beat/index.tsx @@ -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 React from 'react'; +import { PrimaryLayout } from '../../components/layouts/primary'; + +export class BeatDetailsPage extends React.PureComponent { + public render() { + return ( + +
beat details 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 index 7854e3336f070..e8524299a0afe 100644 --- a/x-pack/plugins/beats_management/public/pages/main/beats.tsx +++ b/x-pack/plugins/beats_management/public/pages/main/beats.tsx @@ -11,7 +11,7 @@ import { } from '@elastic/eui'; import React from 'react'; -import { BeatTag, CMBeat, CMPopulatedBeat } from '../../../common/domain_types'; +import { BeatTag, 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'; @@ -23,19 +23,19 @@ interface BeatsPageProps { } interface BeatsPageState { - beats: CMBeat[]; - tableRef: any; + beats: CMPopulatedBeat[]; tags: any[] | null; } export class BeatsPage extends React.PureComponent { public static ActionArea = BeatsActionArea; + private tableRef = React.createRef(); + constructor(props: BeatsPageProps) { super(props); this.state = { beats: [], - tableRef: React.createRef(), tags: null, }; @@ -51,9 +51,32 @@ export class BeatsPage extends React.PureComponent { + const selectedBeats = this.getSelectedBeats(); + const hasMatches = selectedBeats.some((beat: any) => + (beat.tags || []).some((t: string) => t === tag.id) + ); + + return ( + + this.removeTagsFromBeats(selectedBeats, tag) + : () => this.assignTagsToBeats(selectedBeats, tag) + } + onClickAriaLabel={tag.id} + > + {tag.id} + + + ); + }} assignmentTitle="Set tags" items={this.state.beats || []} - ref={this.state.tableRef} + ref={this.tableRef} showAssignmentOptions={true} type={BeatsTableType} /> @@ -88,6 +111,9 @@ export class BeatsPage extends React.PureComponent { await this.loadBeats(); + if (this.tableRef && this.tableRef.current) { + this.tableRef.current.resetSelection(); + } }, 100); }; @@ -98,39 +124,16 @@ export class BeatsPage extends React.PureComponent { // 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, + tags, }); }; @@ -146,10 +149,16 @@ export class BeatsPage extends React.PureComponent { await this.props.libs.beats.assignTagsToBeats(this.createBeatTagAssignments(beats, tag)); - this.loadBeats(); + await this.loadBeats(); + await this.loadTags(); }; private getSelectedBeats = (): CMPopulatedBeat[] => { - return this.state.tableRef.current.state.selection; + if (this.tableRef && this.tableRef.current) { + return this.tableRef.current.state.selection.map( + (beat: CMPopulatedBeat) => this.state.beats.find(b => b.id === beat.id) || beat + ); + } + return []; }; } 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 index 6cedb5bd363b9..d7973a92c7e50 100644 --- 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 @@ -75,7 +75,7 @@ export class BeatsActionArea extends React.Component { color="primary" onClick={async () => { const token = await libs.tokens.createEnrollmentToken(); - history.push(`/beats/enroll/${token}`); + history.push(`/overview/beats/enroll/${token}`); this.waitForToken(token); }} > @@ -88,7 +88,7 @@ export class BeatsActionArea extends React.Component { this.pinging = false; this.setState({ enrolledBeat: null - }, () => history.push('/beats')) + }, () => history.push('/overview/beats')) }} style={{ width: '640px' }}> Enroll a new Beat @@ -146,7 +146,7 @@ export class BeatsActionArea extends React.Component { enrolledBeat: null }) const token = await libs.tokens.createEnrollmentToken(); - history.push(`/beats/enroll/${token}`); + history.push(`/overview/beats/enroll/${token}`); this.waitForToken(token); }} > 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 deleted file mode 100644 index 27ba69e435eef..0000000000000 --- a/x-pack/plugins/beats_management/public/pages/main/create_tag.tsx +++ /dev/null @@ -1,40 +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 'brace/mode/yaml'; -import 'brace/theme/github'; -import React from 'react'; -import { ConfigurationBlock } from '../../../common/domain_types'; -import { TagCreateConfig, TagEdit } from '../../components/tag'; -import { FrontendLibs } from '../../lib/lib'; - -interface CreateTagPageProps { - libs: FrontendLibs; -} - -interface CreateTagPageState { - color: string | null; - configurationBlocks: ConfigurationBlock[]; - showFlyout: boolean; - tagName: string | null; -} - -export class CreateTagPage extends React.PureComponent { - constructor(props: CreateTagPageProps) { - 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 7d201bde445be..dfac6ac994e2f 100644 --- a/x-pack/plugins/beats_management/public/pages/main/index.tsx +++ b/x-pack/plugins/beats_management/public/pages/main/index.tsx @@ -11,13 +11,11 @@ import { EuiTabs, } from '@elastic/eui'; import React from 'react'; -import { Redirect, Route, Switch } from 'react-router-dom'; +import { 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 { CreateTagPage } from './create_tag'; -import { EditTagPage } from './edit_tag'; import { TagsPage } from './tags'; interface MainPagesProps { @@ -26,7 +24,6 @@ interface MainPagesProps { } interface MainPagesState { - selectedTabId: string; enrollBeat?: { enrollmentToken: string; } | null; @@ -35,10 +32,6 @@ interface MainPagesState { export class MainPages extends React.PureComponent { constructor(props: any) { super(props); - - this.state = { - selectedTabId: '/', - }; } public onSelectedTabChanged = (id: string) => { @@ -48,30 +41,20 @@ export class MainPages extends React.PureComponent ( @@ -90,9 +73,13 @@ export class MainPages extends React.PureComponent } /> + } + /> } > @@ -100,34 +87,19 @@ export class MainPages extends React.PureComponent } - /> - } /> } /> } /> - } - /> - } - /> ); diff --git a/x-pack/plugins/beats_management/public/pages/main/tags.tsx b/x-pack/plugins/beats_management/public/pages/main/tags.tsx index 9c8c0ac2347b1..73b39a215a46d 100644 --- a/x-pack/plugins/beats_management/public/pages/main/tags.tsx +++ b/x-pack/plugins/beats_management/public/pages/main/tags.tsx @@ -4,9 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ - -// @ts-ignore EuiToolTip has no typings in current version -import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiToolTip } from '@elastic/eui'; +import { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + // @ts-ignore EuiToolTip has no typings in current version + EuiToolTip, +} from '@elastic/eui'; import React from 'react'; import { BeatTag, CMBeat } from '../../../common/domain_types'; import { BeatsTagAssignment } from '../../../server/lib/adapters/beats/adapter_types'; @@ -19,17 +25,27 @@ interface TagsPageProps { interface TagsPageState { beats: any; - tableRef: any; tags: BeatTag[]; } export class TagsPage extends React.PureComponent { + public static ActionArea = ({ history }: any) => ( + { + history.push(`/tag/create`); + }} + > + Create Tag + + ); + public tableRef = React.createRef
(); constructor(props: TagsPageProps) { super(props); this.state = { beats: [], - tableRef: React.createRef(), tags: [], }; @@ -41,20 +57,34 @@ export class TagsPage extends React.PureComponent
); } - private handleTagsAction = (action: string, payload: any) => { + private handleTagsAction = async (action: string) => { switch (action) { case 'loadAssignmentOptions': this.loadBeats(); break; + case 'delete': + const tags = this.getSelectedTags().map(tag => tag.id); + const success = await this.props.libs.tags.delete(tags); + if (!success) { + alert( + 'Some of these tags might be assigned to beats. Please ensure tags being removed are not activly assigned' + ); + } else { + this.loadTags(); + if (this.tableRef && this.tableRef.current) { + this.tableRef.current.resetSelection(); + } + } + break; } this.loadTags(); @@ -136,7 +166,10 @@ export class TagsPage extends React.PureComponent await this.props.libs.beats.assignTagsToBeats(assignments); }; - private getSelectedTags = () => { - return this.state.tableRef.current.state.selection; + private getSelectedTags = (): BeatTag[] => { + if (this.tableRef && this.tableRef.current) { + return this.tableRef.current.state.selection; + } + return []; }; } diff --git a/x-pack/plugins/beats_management/public/pages/tag/create.tsx b/x-pack/plugins/beats_management/public/pages/tag/create.tsx new file mode 100644 index 0000000000000..5a319a04aad7f --- /dev/null +++ b/x-pack/plugins/beats_management/public/pages/tag/create.tsx @@ -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 { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import 'brace/mode/yaml'; +import 'brace/theme/github'; +import React from 'react'; +import { BeatTag } from '../../../common/domain_types'; +import { TagEdit } from '../../components/tag'; +import { FrontendLibs } from '../../lib/lib'; + +interface CreateTagPageProps { + libs: FrontendLibs; + history: any; +} + +interface CreateTagPageState { + showFlyout: boolean; + tag: BeatTag; +} + +export class CreateTagPage extends React.PureComponent { + constructor(props: CreateTagPageProps) { + super(props); + + this.state = { + showFlyout: false, + tag: { + id: '', + color: '#DD0A73', + configuration_blocks: [], + last_updated: new Date(), + }, + }; + } + + public render() { + return ( +
+ + this.setState(oldState => ({ + tag: { ...oldState.tag, [field]: value }, + })) + } + attachedBeats={null} + /> + + + + + Save + + + + this.props.history.push('/overview/tags')}> + Cancel + + + +
+ ); + } + + private saveTag = async () => { + await this.props.libs.tags.upsertTag(this.state.tag as BeatTag); + this.props.history.push('/overview/tags'); + }; +} diff --git a/x-pack/plugins/beats_management/public/pages/main/edit_tag.tsx b/x-pack/plugins/beats_management/public/pages/tag/edit.tsx similarity index 58% rename from x-pack/plugins/beats_management/public/pages/main/edit_tag.tsx rename to x-pack/plugins/beats_management/public/pages/tag/edit.tsx index dab4d82e36844..386cf2cc18eb0 100644 --- a/x-pack/plugins/beats_management/public/pages/main/edit_tag.tsx +++ b/x-pack/plugins/beats_management/public/pages/tag/edit.tsx @@ -7,8 +7,8 @@ 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 { BeatTag } from '../../../common/domain_types'; +import { TagEdit } from '../../components/tag'; import { FrontendLibs } from '../../lib/lib'; interface EditTagPageProps { @@ -16,10 +16,8 @@ interface EditTagPageProps { } interface EditTagPageState { - color: string | null; - configurationBlocks: ConfigurationBlock[]; showFlyout: boolean; - tagName: string | null; + tag: Partial; } export class EditTagPage extends React.PureComponent { @@ -27,14 +25,26 @@ export class EditTagPage extends React.PureComponent; + return ( + + this.setState(oldState => ({ + tag: { ...oldState.tag, [field]: value }, + })) + } + attachedBeats={[]} + /> + ); } } diff --git a/x-pack/plugins/beats_management/public/pages/tag/index.tsx b/x-pack/plugins/beats_management/public/pages/tag/index.tsx new file mode 100644 index 0000000000000..1a1054cd1ab28 --- /dev/null +++ b/x-pack/plugins/beats_management/public/pages/tag/index.tsx @@ -0,0 +1,58 @@ +/* + * 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 { Route, Switch } from 'react-router'; +import { ConfigurationBlock } from '../../../common/domain_types'; +import { PrimaryLayout } from '../../components/layouts/primary'; +import { FrontendLibs } from '../../lib/lib'; +import { CreateTagPage } from './create'; +import { EditTagPage } from './edit'; +interface EditTagPageProps { + libs: FrontendLibs; + history: any; +} + +interface EditTagPageState { + color: string | null; + configurationBlocks: ConfigurationBlock[]; + showFlyout: boolean; + tagName: string | null; +} + +export class TagPage 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/router.tsx b/x-pack/plugins/beats_management/public/router.tsx index 7fb283c33da86..6642384468803 100644 --- a/x-pack/plugins/beats_management/public/router.tsx +++ b/x-pack/plugins/beats_management/public/router.tsx @@ -5,14 +5,28 @@ */ import React from 'react'; -import { HashRouter, Route } from 'react-router-dom'; +import { HashRouter, Redirect, Route, Switch } from 'react-router-dom'; +import { BeatDetailsPage } from './pages/beat'; import { MainPages } from './pages/main'; +import { TagPage } from './pages/tag'; export const PageRouter: React.SFC<{ libs: any }> = ({ libs }) => { return ( - } /> + + } + /> + } /> + } + /> + } /> + ); }; 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 9fc2578ccf8da..c8d96a77df9f7 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 @@ -13,6 +13,7 @@ export interface CMBeatsAdapter { get(user: FrameworkUser, id: string): Promise; getAll(user: FrameworkUser): Promise; getWithIds(user: FrameworkUser, beatIds: string[]): Promise; + getAllWithTags(user: FrameworkUser, tagIds: string[]): Promise; getBeatWithToken(user: FrameworkUser, enrollmentToken: string): Promise; removeTagsFromBeats( user: FrameworkUser, 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 22bba3661a752..c7f728c9c00d8 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 @@ -84,6 +84,28 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { .map((b: any) => b._source.beat); } + public async getAllWithTags(user: FrameworkUser, tagIds: string[]): Promise { + const params = { + ignore: [404], + index: INDEX_NAMES.BEATS, + type: '_doc', + body: { + query: { + terms: { 'beat.tags': tagIds }, + }, + }, + }; + + const response = await this.database.search(user, params); + + const beats = _get(response, 'hits.hits', []); + + if (beats.length === 0) { + return []; + } + return beats.map((beat: any) => omit(beat._source.beat, ['access_token'])); + } + public async getBeatWithToken( user: FrameworkUser, enrollmentToken: string @@ -106,7 +128,7 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { if (beats.length === 0) { return null; } - return _get(beats[0], '_source.beat'); + return omit(_get(beats[0], '_source.beat'), ['access_token']); } public async getAll(user: FrameworkUser) { 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 3ab38a0716455..f5bcf91e07d52 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 @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { omit } from 'lodash'; +import { intersection, omit } from 'lodash'; import { CMBeat } from '../../../../common/domain_types'; import { FrameworkUser } from '../framework/adapter_types'; @@ -38,6 +38,10 @@ export class MemoryBeatsAdapter implements CMBeatsAdapter { return this.beatsDB.filter(beat => beatIds.includes(beat.id)); } + public async getAllWithTags(user: FrameworkUser, tagIds: string[]): Promise { + return this.beatsDB.filter(beat => intersection(tagIds, beat.tags || []).length !== 0); + } + public async getBeatWithToken( user: FrameworkUser, enrollmentToken: string 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 77ae4ff8ad095..a5e01541ce386 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 @@ -8,6 +8,7 @@ import { FrameworkUser } from '../framework/adapter_types'; export interface CMTagsAdapter { getAll(user: FrameworkUser): Promise; + delete(user: FrameworkUser, tagIds: string[]): 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 645ed37f5ff8f..c53cd2836d9ba 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 @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get } from 'lodash'; +import { flatten, get } from 'lodash'; import { INDEX_NAMES } from '../../../../common/constants'; import { FrameworkUser } from '../framework/adapter_types'; -import { BeatTag } from '../../../../common/domain_types'; +import { BeatTag, CMBeat } from '../../../../common/domain_types'; import { DatabaseAdapter } from '../database/adapter_types'; import { CMTagsAdapter } from './adapter_types'; @@ -21,13 +21,71 @@ export class ElasticsearchTagsAdapter implements CMTagsAdapter { public async getAll(user: FrameworkUser) { const params = { + _source: true, index: INDEX_NAMES.BEATS, q: 'type:tag', type: '_doc', }; const response = await this.database.search(user, params); + const tags = get(response, 'hits.hits', []); - return get(response, 'hits.hits', []); + return tags.map((tag: any) => tag._source.tag); + } + + public async delete(user: FrameworkUser, tagIds: string[]) { + const ids = tagIds.map(tag => tag); + + const params = { + ignore: [404], + index: INDEX_NAMES.BEATS, + type: '_doc', + body: { + query: { + terms: { 'beat.tags': tagIds }, + }, + }, + }; + + const beatsResponse = await this.database.search(user, params); + + const beats = get(beatsResponse, 'hits.hits', []).map( + (beat: any) => beat._source.beat + ); + + const inactiveBeats = beats.filter(beat => beat.active === false); + const activeBeats = beats.filter(beat => beat.active === true); + if (activeBeats.length !== 0) { + return false; + } + const beatIds = inactiveBeats.map((beat: CMBeat) => beat.id); + + const bulkBeatsUpdates = flatten( + beatIds.map(beatId => { + const script = ` + def beat = ctx._source.beat; + if (beat.tags != null) { + beat.tags.removeAll([params.tag]); + }`; + + return flatten( + ids.map(tagId => [ + { update: { _id: `beat:${beatId}` } }, + { script: { source: script.replace(' ', ''), params: { tagId } } }, + ]) + ); + }) + ); + + const bulkTagsDelete = ids.map(tagId => ({ delete: { _id: `tag:${tagId}` } })); + + await this.database.bulk(user, { + body: flatten([...bulkBeatsUpdates, ...bulkTagsDelete]), + index: INDEX_NAMES.BEATS, + refresh: 'wait_for', + type: '_doc', + }); + + return true; } public async getTagsWithIds(user: FrameworkUser, tagIds: string[]) { @@ -35,7 +93,7 @@ export class ElasticsearchTagsAdapter implements CMTagsAdapter { // TODO abstract to kibana adapter as the more generic getDocs const params = { - _sourceInclude: ['tag.configuration_blocks'], + _source: true, body: { ids, }, 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 7623c8aaa21d8..4d2ed434b8760 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 @@ -18,7 +18,11 @@ export class MemoryTagsAdapter implements CMTagsAdapter { public async getAll(user: FrameworkUser) { return this.tagsDB; } + public async delete(user: FrameworkUser, tagIds: string[]) { + this.tagsDB = this.tagsDB.filter(tag => !tagIds.includes(tag.id)); + return true; + } 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/domains/beats.ts b/x-pack/plugins/beats_management/server/lib/domains/beats.ts index 199077d8570c3..bd04b60aa6468 100644 --- a/x-pack/plugins/beats_management/server/lib/domains/beats.ts +++ b/x-pack/plugins/beats_management/server/lib/domains/beats.ts @@ -49,6 +49,11 @@ export class CMBeatsDomain { return beat && beat.active ? beat : null; } + public async getAllWithTags(user: FrameworkUser, tagIds: string[], justActive = true) { + const beats = await this.adapter.getAllWithTags(user, tagIds); + return justActive ? beats.filter((beat: CMBeat) => beat.active === true) : beats; + } + public async update(userOrToken: UserOrToken, beatId: string, beatData: Partial) { const beat = await this.adapter.get(this.framework.internalUser, beatId); 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 5f5e6747cc847..192b16c0b9ed9 100644 --- a/x-pack/plugins/beats_management/server/lib/domains/tags.ts +++ b/x-pack/plugins/beats_management/server/lib/domains/tags.ts @@ -13,10 +13,7 @@ import { entries } from '../../utils/polyfills'; import { CMTagsAdapter } from '../adapters/tags/adapter_types'; export class CMTagsDomain { - private adapter: CMTagsAdapter; - constructor(adapter: CMTagsAdapter) { - this.adapter = adapter; - } + constructor(private readonly adapter: CMTagsAdapter) {} public async getAll(user: FrameworkUser) { return await this.adapter.getAll(user); @@ -26,14 +23,24 @@ 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 delete(user: FrameworkUser, tagIds: string[]) { + return await this.adapter.delete(user, tagIds); + } + + public async saveTag( + user: FrameworkUser, + tagId: string, + config: { color: string; configuration_blocks: ConfigurationBlock[] } + ) { + const { isValid, message } = await this.validateConfigurationBlocks( + config.configuration_blocks + ); if (!isValid) { return { isValid, result: message }; } const tag = { - configuration_blocks: configs, + ...config, id: tagId, last_updated: new Date(), }; diff --git a/x-pack/plugins/beats_management/server/management_server.ts b/x-pack/plugins/beats_management/server/management_server.ts index 2cc4676da359e..bc14bacdb9e7b 100644 --- a/x-pack/plugins/beats_management/server/management_server.ts +++ b/x-pack/plugins/beats_management/server/management_server.ts @@ -12,6 +12,7 @@ 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 { createDeleteTagsWithIdsRoute } from './rest_api/tags/delete'; import { createGetTagsWithIdsRoute } from './rest_api/tags/get'; import { createListTagsRoute } from './rest_api/tags/list'; import { createSetTagRoute } from './rest_api/tags/set'; @@ -29,6 +30,7 @@ export const initManagementServer = (libs: CMServerLibs) => { libs.framework.registerRoute(createGetBeatRoute(libs)); libs.framework.registerRoute(createGetTagsWithIdsRoute(libs)); libs.framework.registerRoute(createListTagsRoute(libs)); + libs.framework.registerRoute(createDeleteTagsWithIdsRoute(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/tag_removal.ts b/x-pack/plugins/beats_management/server/rest_api/beats/tag_removal.ts index eaec8f2872172..0cdd9f2f28c2b 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 @@ -19,7 +19,7 @@ export const createTagRemovalsRoute = (libs: CMServerLibs) => ({ payload: Joi.object({ removals: Joi.array().items( Joi.object({ - beat_id: Joi.string().required(), + beatId: Joi.string().required(), tag: Joi.string().required(), }) ), @@ -29,14 +29,8 @@ export const createTagRemovalsRoute = (libs: CMServerLibs) => ({ handler: async (request: FrameworkRequest, 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.user, tweakedRemovals); + const response = await libs.beats.removeTagsFromBeats(request.user, removals); reply(response); } catch (err) { // TODO move this to kibana route thing in adapter diff --git a/x-pack/plugins/beats_management/server/rest_api/tags/delete.ts b/x-pack/plugins/beats_management/server/rest_api/tags/delete.ts new file mode 100644 index 0000000000000..451d800122a04 --- /dev/null +++ b/x-pack/plugins/beats_management/server/rest_api/tags/delete.ts @@ -0,0 +1,26 @@ +/* + * 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'; + +export const createDeleteTagsWithIdsRoute = (libs: CMServerLibs) => ({ + method: 'DELETE', + 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 success: boolean; + try { + success = await libs.tags.delete(request.user, tagIds); + } catch (err) { + return reply(wrapEsError(err)); + } + + reply({ success }); + }, +}); 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 index 41874a77eef0f..0569e29bb60db 100644 --- a/x-pack/plugins/beats_management/server/rest_api/tags/list.ts +++ b/x-pack/plugins/beats_management/server/rest_api/tags/list.ts @@ -10,7 +10,7 @@ import { wrapEsError } from '../../utils/error_wrappers'; export const createListTagsRoute = (libs: CMServerLibs) => ({ method: 'GET', - path: '/api/beats/tags/', + path: '/api/beats/tags', handler: async (request: any, reply: any) => { let tags: BeatTag[]; try { 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 f50721e764ef3..25837100e2a60 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 @@ -22,6 +22,7 @@ export const createSetTagRoute = (libs: CMServerLibs) => ({ tag: Joi.string(), }), payload: Joi.object({ + color: Joi.string(), configuration_blocks: Joi.array().items( Joi.object({ block_yml: Joi.string().required(), @@ -34,18 +35,14 @@ export const createSetTagRoute = (libs: CMServerLibs) => ({ }, }, handler: async (request: FrameworkRequest, reply: any) => { - const configurationBlocks = get(request, 'payload.configuration_blocks', []); + const config = get(request, 'payload', { configuration_blocks: [], color: '#DD0A73' }); try { - const { isValid, result } = await libs.tags.saveTag( - request.user, - request.params.tag, - configurationBlocks - ); + const { isValid, result } = await libs.tags.saveTag(request.user, request.params.tag, config); if (!isValid) { - return reply({ result }).code(400); + return reply({ result, success: false }).code(400); } - reply().code(result === 'created' ? 201 : 200); + reply({ success: true }).code(result === 'created' ? 201 : 200); } catch (err) { // TODO move this to kibana route thing in adapter return reply(wrapEsError(err)); diff --git a/x-pack/plugins/beats_management/server/utils/adapters/beats/elasticsearch_beats_adapter.ts b/x-pack/plugins/beats_management/server/utils/adapters/beats/elasticsearch_beats_adapter.ts new file mode 100644 index 0000000000000..283f65c1258ae --- /dev/null +++ b/x-pack/plugins/beats_management/server/utils/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_management/server/utils/adapters/famework/kibana/kibana_framework_adapter.ts b/x-pack/plugins/beats_management/server/utils/adapters/famework/kibana/kibana_framework_adapter.ts new file mode 100644 index 0000000000000..6fc2fc4853b03 --- /dev/null +++ b/x-pack/plugins/beats_management/server/utils/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_management/server/utils/adapters/tags/elasticsearch_tags_adapter.ts b/x-pack/plugins/beats_management/server/utils/adapters/tags/elasticsearch_tags_adapter.ts new file mode 100644 index 0000000000000..2293ba77677fd --- /dev/null +++ b/x-pack/plugins/beats_management/server/utils/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_management/server/utils/adapters/tokens/elasticsearch_tokens_adapter.ts b/x-pack/plugins/beats_management/server/utils/adapters/tokens/elasticsearch_tokens_adapter.ts new file mode 100644 index 0000000000000..c8969c7ab08d0 --- /dev/null +++ b/x-pack/plugins/beats_management/server/utils/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_management/server/utils/compose/kibana.ts b/x-pack/plugins/beats_management/server/utils/compose/kibana.ts new file mode 100644 index 0000000000000..ff478646aea89 --- /dev/null +++ b/x-pack/plugins/beats_management/server/utils/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_management/server/utils/domains/beats.ts b/x-pack/plugins/beats_management/server/utils/domains/beats.ts new file mode 100644 index 0000000000000..c0d9ec704e2b1 --- /dev/null +++ b/x-pack/plugins/beats_management/server/utils/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_management/server/utils/domains/tags.ts b/x-pack/plugins/beats_management/server/utils/domains/tags.ts new file mode 100644 index 0000000000000..43bb8dfed15a1 --- /dev/null +++ b/x-pack/plugins/beats_management/server/utils/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_management/server/utils/domains/tokens.ts b/x-pack/plugins/beats_management/server/utils/domains/tokens.ts new file mode 100644 index 0000000000000..6e55d78ecdcc8 --- /dev/null +++ b/x-pack/plugins/beats_management/server/utils/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_management/server/utils/index_templates/beats_template.json b/x-pack/plugins/beats_management/server/utils/index_templates/beats_template.json index 0442c31fd7c2d..1bbe65b70e67d 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 @@ -33,6 +33,9 @@ "color": { "type": "keyword" }, + "last_updated": { + "type": "date" + }, "configuration_blocks": { "type": "nested", "properties": { diff --git a/x-pack/plugins/beats_management/server/utils/lib.ts b/x-pack/plugins/beats_management/server/utils/lib.ts new file mode 100644 index 0000000000000..37d0a989e4cf5 --- /dev/null +++ b/x-pack/plugins/beats_management/server/utils/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/grokdebugger/common/constants/index.js b/x-pack/plugins/grokdebugger/common/constants/index.js deleted file mode 100644 index 12e440d7ed858..0000000000000 --- a/x-pack/plugins/grokdebugger/common/constants/index.js +++ /dev/null @@ -1,9 +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 { ROUTES } from './routes'; -export { PLUGIN } from './plugin'; -export { EDITOR } from './editor'; diff --git a/x-pack/plugins/index_management/server/lib/call_with_request_factory/call_with_request_factory.js b/x-pack/plugins/index_management/server/lib/call_with_request_factory/call_with_request_factory.js deleted file mode 100644 index b9a77a1a0362b..0000000000000 --- a/x-pack/plugins/index_management/server/lib/call_with_request_factory/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 cluster = server.plugins.elasticsearch.getCluster('data'); - return cluster.callWithRequest; -}); - -export const callWithRequestFactory = (server, request) => { - return (...args) => { - return callWithRequest(server)(request, ...args); - }; -}; diff --git a/x-pack/plugins/logstash/common/constants/configuration_blocks.ts b/x-pack/plugins/logstash/common/constants/configuration_blocks.ts new file mode 100644 index 0000000000000..e89e53e25b89d --- /dev/null +++ b/x-pack/plugins/logstash/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/logstash/server/kibana.index.ts b/x-pack/plugins/logstash/server/kibana.index.ts new file mode 100644 index 0000000000000..c9bc9b8bf02f4 --- /dev/null +++ b/x-pack/plugins/logstash/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/logstash/server/management_server.ts b/x-pack/plugins/logstash/server/management_server.ts new file mode 100644 index 0000000000000..ed0917eda8ced --- /dev/null +++ b/x-pack/plugins/logstash/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/logstash/server/rest_api/beats/enroll.ts b/x-pack/plugins/logstash/server/rest_api/beats/enroll.ts new file mode 100644 index 0000000000000..fe154592564ae --- /dev/null +++ b/x-pack/plugins/logstash/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/logstash/server/rest_api/beats/list.ts b/x-pack/plugins/logstash/server/rest_api/beats/list.ts new file mode 100644 index 0000000000000..8263d1c0ff63f --- /dev/null +++ b/x-pack/plugins/logstash/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/logstash/server/rest_api/beats/tag_assignment.ts b/x-pack/plugins/logstash/server/rest_api/beats/tag_assignment.ts new file mode 100644 index 0000000000000..d06c016ce6d12 --- /dev/null +++ b/x-pack/plugins/logstash/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/logstash/server/rest_api/beats/tag_removal.ts b/x-pack/plugins/logstash/server/rest_api/beats/tag_removal.ts new file mode 100644 index 0000000000000..4da33dbd50cfc --- /dev/null +++ b/x-pack/plugins/logstash/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/logstash/server/rest_api/beats/update.ts b/x-pack/plugins/logstash/server/rest_api/beats/update.ts new file mode 100644 index 0000000000000..41d403399d45f --- /dev/null +++ b/x-pack/plugins/logstash/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/logstash/server/rest_api/beats/verify.ts b/x-pack/plugins/logstash/server/rest_api/beats/verify.ts new file mode 100644 index 0000000000000..866fa77d0c337 --- /dev/null +++ b/x-pack/plugins/logstash/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/logstash/server/rest_api/tags/set.ts b/x-pack/plugins/logstash/server/rest_api/tags/set.ts new file mode 100644 index 0000000000000..3f7e579bd91ae --- /dev/null +++ b/x-pack/plugins/logstash/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/logstash/server/rest_api/tokens/create.ts b/x-pack/plugins/logstash/server/rest_api/tokens/create.ts new file mode 100644 index 0000000000000..b4f3e2c1a6246 --- /dev/null +++ b/x-pack/plugins/logstash/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/logstash/server/utils/README.md b/x-pack/plugins/logstash/server/utils/README.md new file mode 100644 index 0000000000000..8a6a27aa29867 --- /dev/null +++ b/x-pack/plugins/logstash/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/logstash/server/utils/find_non_existent_items.ts b/x-pack/plugins/logstash/server/utils/find_non_existent_items.ts new file mode 100644 index 0000000000000..53e4066acc879 --- /dev/null +++ b/x-pack/plugins/logstash/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/logstash/common/constants/index_names.js b/x-pack/plugins/logstash/server/utils/index_templates/index.ts similarity index 73% rename from x-pack/plugins/logstash/common/constants/index_names.js rename to x-pack/plugins/logstash/server/utils/index_templates/index.ts index 0f6946f407c58..eeaef7a68d49f 100644 --- a/x-pack/plugins/logstash/common/constants/index_names.js +++ b/x-pack/plugins/logstash/server/utils/index_templates/index.ts @@ -4,6 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export const INDEX_NAMES = { - PIPELINES: '.logstash', -}; +import beatsIndexTemplate from './beats_template.json'; +export { beatsIndexTemplate }; diff --git a/x-pack/plugins/logstash/server/utils/polyfills.ts b/x-pack/plugins/logstash/server/utils/polyfills.ts new file mode 100644 index 0000000000000..5291e2c72be7d --- /dev/null +++ b/x-pack/plugins/logstash/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/logstash/server/utils/wrap_request.ts b/x-pack/plugins/logstash/server/utils/wrap_request.ts new file mode 100644 index 0000000000000..a29f9055f3688 --- /dev/null +++ b/x-pack/plugins/logstash/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/logstash/tsconfig.json b/x-pack/plugins/logstash/tsconfig.json new file mode 100644 index 0000000000000..4082f16a5d91c --- /dev/null +++ b/x-pack/plugins/logstash/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.json" +} diff --git a/x-pack/plugins/logstash/wallaby.js b/x-pack/plugins/logstash/wallaby.js new file mode 100644 index 0000000000000..c20488d35cfb6 --- /dev/null +++ b/x-pack/plugins/logstash/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/plugins/reporting/server/browsers/chromium/driver/screenshot_stitcher/index.test.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver/screenshot_stitcher/index.test.ts index b519a0f6363a5..2fb34a026a502 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver/screenshot_stitcher/index.test.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver/screenshot_stitcher/index.test.ts @@ -8,7 +8,7 @@ import { promisify } from 'bluebird'; import fs from 'fs'; import path from 'path'; -import { screenshotStitcher } from './index'; +import { screenshotStitcher } from '.'; const loggerMock = { debug: () => { 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 index 19583f9279732..95030a63ac749 100644 --- 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 @@ -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 remove a single tag from a single beat', async () => { const { body: apiResponse } = await supertest - .post( - '/api/beats/agents_tags/removals' - ) + .post('/api/beats/agents_tags/removals') .set('kbn-xsrf', 'xxx') .send({ - removals: [ - { beat_id: 'foo', tag: 'production' } - ] + removals: [{ beatId: 'foo', tag: 'production' }], }) .expect(200); - expect(apiResponse.removals).to.eql([ - { status: 200, result: 'updated' } - ]); + 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` + id: `beat:foo`, }); const beat = esResponse._source.beat; @@ -51,21 +42,16 @@ export default function ({ getService }) { it('should remove a single tag from a multiple beats', async () => { const { body: apiResponse } = await supertest - .post( - '/api/beats/agents_tags/removals' - ) + .post('/api/beats/agents_tags/removals') .set('kbn-xsrf', 'xxx') .send({ - removals: [ - { beat_id: 'foo', tag: 'development' }, - { beat_id: 'bar', tag: 'development' } - ] + removals: [{ beatId: 'foo', tag: 'development' }, { beatId: 'bar', tag: 'development' }], }) .expect(200); expect(apiResponse.removals).to.eql([ { status: 200, result: 'updated' }, - { status: 200, result: 'updated' } + { status: 200, result: 'updated' }, ]); let esResponse; @@ -75,17 +61,17 @@ 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; - expect(beat.tags).to.eql(['production', 'qa' ]); // as beat 'foo' already had 'production' and 'qa' tags attached to it + 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` + id: `beat:bar`, }); beat = esResponse._source.beat; @@ -94,27 +80,22 @@ export default function ({ getService }) { it('should remove multiple tags from a single beat', async () => { const { body: apiResponse } = await supertest - .post( - '/api/beats/agents_tags/removals' - ) + .post('/api/beats/agents_tags/removals') .set('kbn-xsrf', 'xxx') .send({ - removals: [ - { beat_id: 'foo', tag: 'development' }, - { beat_id: 'foo', tag: 'production' } - ] + removals: [{ beatId: 'foo', tag: 'development' }, { beatId: 'foo', tag: 'production' }], }) .expect(200); expect(apiResponse.removals).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:foo` + id: `beat:foo`, }); const beat = esResponse._source.beat; @@ -123,21 +104,16 @@ export default function ({ getService }) { it('should remove multiple tags from a multiple beats', async () => { const { body: apiResponse } = await supertest - .post( - '/api/beats/agents_tags/removals' - ) + .post('/api/beats/agents_tags/removals') .set('kbn-xsrf', 'xxx') .send({ - removals: [ - { beat_id: 'foo', tag: 'production' }, - { beat_id: 'bar', tag: 'development' } - ] + removals: [{ beatId: 'foo', tag: 'production' }, { beatId: 'bar', tag: 'development' }], }) .expect(200); expect(apiResponse.removals).to.eql([ { status: 200, result: 'updated' }, - { status: 200, result: 'updated' } + { status: 200, result: 'updated' }, ]); let esResponse; @@ -147,7 +123,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; @@ -157,7 +133,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; @@ -168,19 +144,15 @@ export default function ({ getService }) { const nonExistentBeatId = chance.word(); const { body: apiResponse } = await supertest - .post( - '/api/beats/agents_tags/removals' - ) + .post('/api/beats/agents_tags/removals') .set('kbn-xsrf', 'xxx') .send({ - removals: [ - { beat_id: nonExistentBeatId, tag: 'production' } - ] + removals: [{ beat_id: nonExistentBeatId, tag: 'production' }], }) .expect(200); expect(apiResponse.removals).to.eql([ - { status: 404, result: `Beat ${nonExistentBeatId} not found` } + { status: 404, result: `Beat ${nonExistentBeatId} not found` }, ]); }); @@ -188,25 +160,21 @@ export default function ({ getService }) { const nonExistentTag = chance.word(); const { body: apiResponse } = await supertest - .post( - '/api/beats/agents_tags/removals' - ) + .post('/api/beats/agents_tags/removals') .set('kbn-xsrf', 'xxx') .send({ - removals: [ - { beat_id: 'bar', tag: nonExistentTag } - ] + removals: [{ beatId: 'bar', tag: nonExistentTag }], }) .expect(200); expect(apiResponse.removals).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; @@ -218,25 +186,21 @@ export default function ({ getService }) { const nonExistentTag = chance.word(); const { body: apiResponse } = await supertest - .post( - '/api/beats/agents_tags/removals' - ) + .post('/api/beats/agents_tags/removals') .set('kbn-xsrf', 'xxx') .send({ - removals: [ - { beat_id: nonExistentBeatId, tag: nonExistentTag } - ] + removals: [{ beatId: nonExistentBeatId, tag: nonExistentTag }], }) .expect(200); expect(apiResponse.removals).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; diff --git a/x-pack/yarn.lock b/x-pack/yarn.lock index 598b142d3ca93..0dd2bc432b2fc 100644 --- a/x-pack/yarn.lock +++ b/x-pack/yarn.lock @@ -231,6 +231,13 @@ "@types/history" "*" "@types/react" "*" +"@types/react-router@^4.0.30": + version "4.0.30" + resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-4.0.30.tgz#64bcd886befd1f932779b74d17954adbefb7a3a7" + dependencies: + "@types/history" "*" + "@types/react" "*" + "@types/react@*": version "16.4.6" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.4.6.tgz#5024957c6bcef4f02823accf5974faba2e54fada"