-
- 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"