diff --git a/x-pack/plugins/endpoint/kibana.json b/x-pack/plugins/endpoint/kibana.json index a7fd20b93f62d..f7a4acd629324 100644 --- a/x-pack/plugins/endpoint/kibana.json +++ b/x-pack/plugins/endpoint/kibana.json @@ -3,7 +3,7 @@ "version": "1.0.0", "kibanaVersion": "kibana", "configPath": ["xpack", "endpoint"], - "requiredPlugins": ["embeddable"], + "requiredPlugins": ["features", "embeddable"], "server": true, "ui": true } diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx new file mode 100644 index 0000000000000..1af27f039aca7 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/index.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; +import ReactDOM from 'react-dom'; +import { CoreStart, AppMountParameters } from 'kibana/public'; +import { I18nProvider, FormattedMessage } from '@kbn/i18n/react'; + +/** + * This module will be loaded asynchronously to reduce the bundle size of your plugin's main bundle. + */ +export function renderApp(coreStart: CoreStart, { element }: AppMountParameters) { + coreStart.http.get('/api/endpoint/hello-world'); + + ReactDOM.render(, element); + + return () => { + ReactDOM.unmountComponentAtNode(element); + }; +} + +const AppRoot = React.memo(() => ( + +

+ +

+
+)); diff --git a/x-pack/plugins/endpoint/public/plugin.ts b/x-pack/plugins/endpoint/public/plugin.ts index 21bf1b3cdea12..02514cc974af0 100644 --- a/x-pack/plugins/endpoint/public/plugin.ts +++ b/x-pack/plugins/endpoint/public/plugin.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Plugin, CoreSetup } from 'kibana/public'; +import { Plugin, CoreSetup, AppMountParameters } from 'kibana/public'; import { IEmbeddableSetup } from 'src/plugins/embeddable/public'; +import { i18n } from '@kbn/i18n'; import { ResolverEmbeddableFactory } from './embeddables/resolver'; export type EndpointPluginStart = void; @@ -24,8 +25,20 @@ export class EndpointPlugin EndpointPluginSetupDependencies, EndpointPluginStartDependencies > { - public setup(_core: CoreSetup, plugins: EndpointPluginSetupDependencies) { + public setup(core: CoreSetup, plugins: EndpointPluginSetupDependencies) { const resolverEmbeddableFactory = new ResolverEmbeddableFactory(); + core.application.register({ + id: 'endpoint', + title: i18n.translate('xpack.endpoint.pluginTitle', { + defaultMessage: 'Endpoint', + }), + async mount(params: AppMountParameters) { + const [coreStart] = await core.getStartServices(); + const { renderApp } = await import('./applications/endpoint'); + return renderApp(coreStart, params); + }, + }); + plugins.embeddable.registerEmbeddableFactory( resolverEmbeddableFactory.type, resolverEmbeddableFactory diff --git a/x-pack/plugins/endpoint/server/index.ts b/x-pack/plugins/endpoint/server/index.ts index f10bc7ee51b2c..eec836141ea5e 100644 --- a/x-pack/plugins/endpoint/server/index.ts +++ b/x-pack/plugins/endpoint/server/index.ts @@ -19,8 +19,8 @@ export const config = { }; export const plugin: PluginInitializer< - EndpointPluginStart, EndpointPluginSetup, - EndpointPluginStartDependencies, - EndpointPluginSetupDependencies + EndpointPluginStart, + EndpointPluginSetupDependencies, + EndpointPluginStartDependencies > = () => new EndpointPlugin(); diff --git a/x-pack/plugins/endpoint/server/plugin.ts b/x-pack/plugins/endpoint/server/plugin.ts index 400b906c5230e..b41dfee1f78fd 100644 --- a/x-pack/plugins/endpoint/server/plugin.ts +++ b/x-pack/plugins/endpoint/server/plugin.ts @@ -3,28 +3,56 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - import { Plugin, CoreSetup } from 'kibana/server'; import { addRoutes } from './routes'; +import { PluginSetupContract as FeaturesPluginSetupContract } from '../../features/server'; export type EndpointPluginStart = void; export type EndpointPluginSetup = void; -export interface EndpointPluginSetupDependencies {} // eslint-disable-line @typescript-eslint/no-empty-interface - export interface EndpointPluginStartDependencies {} // eslint-disable-line @typescript-eslint/no-empty-interface +export interface EndpointPluginSetupDependencies { + features: FeaturesPluginSetupContract; +} + export class EndpointPlugin implements Plugin< - EndpointPluginStart, EndpointPluginSetup, - EndpointPluginStartDependencies, - EndpointPluginSetupDependencies + EndpointPluginStart, + EndpointPluginSetupDependencies, + EndpointPluginStartDependencies > { - public setup(core: CoreSetup) { + public setup(core: CoreSetup, plugins: EndpointPluginSetupDependencies) { + plugins.features.registerFeature({ + id: 'endpoint', + name: 'Endpoint', + icon: 'bug', + navLinkId: 'endpoint', + app: ['endpoint', 'kibana'], + privileges: { + all: { + api: ['resolver'], + savedObject: { + all: [], + read: [], + }, + ui: ['save'], + }, + read: { + api: [], + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + }, + }); const router = core.http.createRouter(); addRoutes(router); } public start() {} + public stop() {} } diff --git a/x-pack/plugins/endpoint/server/routes/index.ts b/x-pack/plugins/endpoint/server/routes/index.ts index 517ee2a853660..8eab6cd384765 100644 --- a/x-pack/plugins/endpoint/server/routes/index.ts +++ b/x-pack/plugins/endpoint/server/routes/index.ts @@ -11,6 +11,9 @@ export function addRoutes(router: IRouter) { { path: '/api/endpoint/hello-world', validate: false, + options: { + tags: ['access:resolver'], + }, }, async function greetingIndex(_context, _request, response) { return response.ok({ diff --git a/x-pack/test/api_integration/apis/features/features/features.ts b/x-pack/test/api_integration/apis/features/features/features.ts index f613473dd87fb..34b2c5e324187 100644 --- a/x-pack/test/api_integration/apis/features/features/features.ts +++ b/x-pack/test/api_integration/apis/features/features/features.ts @@ -114,6 +114,7 @@ export default function({ getService }: FtrProviderContext) { 'maps', 'uptime', 'siem', + 'endpoint', ].sort() ); }); diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index d4c8a3e68c50e..7b1984222404b 100644 --- a/x-pack/test/api_integration/apis/security/privileges.ts +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -37,6 +37,7 @@ export default function({ getService }: FtrProviderContext) { uptime: ['all', 'read'], apm: ['all', 'read'], siem: ['all', 'read'], + endpoint: ['all', 'read'], }, global: ['all', 'read'], space: ['all', 'read'], diff --git a/x-pack/test/functional/apps/endpoint/feature_controls/endpoint_spaces.ts b/x-pack/test/functional/apps/endpoint/feature_controls/endpoint_spaces.ts new file mode 100644 index 0000000000000..6b3b423e293c2 --- /dev/null +++ b/x-pack/test/functional/apps/endpoint/feature_controls/endpoint_spaces.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ getPageObjects, getService }: FtrProviderContext) { + const pageObjects = getPageObjects(['common']); + const spacesService = getService('spaces'); + const testSubjects = getService('testSubjects'); + const appsMenu = getService('appsMenu'); + + describe('spaces', () => { + describe('space with no features disabled', () => { + before(async () => { + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: [], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + }); + + it('shows endpoint navlink', async () => { + await pageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.contain('EEndpoint'); + }); + + it(`endpoint app shows 'Hello World'`, async () => { + await pageObjects.common.navigateToApp('endpoint', { + basePath: '/s/custom_space', + }); + await testSubjects.existOrFail('welcomeTitle'); + }); + }); + + describe('space with endpoint disabled', () => { + before(async () => { + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: ['endpoint'], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + }); + + it(`doesn't show endpoint navlink`, async () => { + await pageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).not.to.contain('EEndpoint'); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/endpoint/feature_controls/index.ts b/x-pack/test/functional/apps/endpoint/feature_controls/index.ts new file mode 100644 index 0000000000000..5f7e611fd966c --- /dev/null +++ b/x-pack/test/functional/apps/endpoint/feature_controls/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('feature controls', function() { + this.tags('skipFirefox'); + loadTestFile(require.resolve('./endpoint_spaces')); + }); +} diff --git a/x-pack/test/functional/apps/endpoint/index.ts b/x-pack/test/functional/apps/endpoint/index.ts new file mode 100644 index 0000000000000..1a0d3e973285b --- /dev/null +++ b/x-pack/test/functional/apps/endpoint/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 { FtrProviderContext } from '../../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('endpoint', function() { + this.tags('ciGroup7'); + + loadTestFile(require.resolve('./feature_controls')); + }); +} diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index 8981c94b83578..17235c61c7d8c 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -56,6 +56,7 @@ export default async function({ readConfigFile }) { resolve(__dirname, './apps/cross_cluster_replication'), resolve(__dirname, './apps/remote_clusters'), resolve(__dirname, './apps/transform'), + resolve(__dirname, './apps/endpoint'), // This license_management file must be last because it is destructive. resolve(__dirname, './apps/license_management'), ], @@ -86,6 +87,7 @@ export default async function({ readConfigFile }) { '--xpack.encryptedSavedObjects.encryptionKey="DkdXazszSCYexXqz4YktBGHCRkV6hyNK"', '--telemetry.banner=false', '--timelion.ui.enabled=true', + '--xpack.endpoint.enabled=true', ], }, uiSettings: { @@ -197,6 +199,9 @@ export default async function({ readConfigFile }) { pathname: '/app/kibana/', hash: '/management/elasticsearch/transform', }, + endpoint: { + pathname: '/app/endpoint', + }, }, // choose where esArchiver should load archives from