diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index c698e2db86ddb..f62a4d28dfc0d 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -116,7 +116,8 @@ cluster alert notifications from Monitoring. ==== Dashboard [horizontal] -`xpackDashboardMode:roles`:: The roles that belong to <>. +`xpackDashboardMode:roles`:: **Deprecated. Use <> instead.** +The roles that belong to <>. [float] [[kibana-discover-settings]] diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 3521d7ef9c66e..9b672d40961d8 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -120,6 +120,7 @@ export class DocLinksService { }, management: { kibanaSearchSettings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/advanced-options.html#kibana-search-settings`, + dashboardSettings: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/advanced-options.html#kibana-dashboard-settings`, }, }, }); diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index fb91b865097fa..35ac4e27f9c8b 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -20,6 +20,7 @@ export const storybookAliases = { apm: 'x-pack/legacy/plugins/apm/scripts/storybook.js', canvas: 'x-pack/legacy/plugins/canvas/scripts/storybook_new.js', + codeeditor: 'src/plugins/kibana_react/public/code_editor/scripts/storybook.ts', drilldowns: 'x-pack/plugins/drilldowns/scripts/storybook.js', embeddable: 'src/plugins/embeddable/scripts/storybook.js', infra: 'x-pack/legacy/plugins/infra/scripts/storybook.js', diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx index 362bc1f21b428..ffe2a153a87f3 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx @@ -486,7 +486,7 @@ class FilterEditorUI extends Component { operator.type, operator.negate, this.props.filter.meta.disabled, - params, + params ?? '', alias, $state.store ); diff --git a/src/plugins/es_ui_shared/public/request/request.test.js b/src/plugins/es_ui_shared/public/request/request.test.js index 44bf149d5fd1e..cc554b531d88a 100644 --- a/src/plugins/es_ui_shared/public/request/request.test.js +++ b/src/plugins/es_ui_shared/public/request/request.test.js @@ -18,7 +18,7 @@ */ import sinon from 'sinon'; -import { sendRequest as sendRequestUnbound, useRequest as useRequestUnbound } from './request'; +// import { sendRequest as sendRequestUnbound, useRequest as useRequestUnbound } from './request'; import React from 'react'; import { act } from 'react-dom/test-utils'; @@ -52,6 +52,11 @@ describe.skip('request lib', () => { let sendRequest; let useRequest; + /** + * + * commented out due to hooks being called regardless of skip + * https://github.com/facebook/jest/issues/8379 + beforeEach(() => { sendPost = sinon.stub(); sendPost.withArgs(successRequest.path, successRequest.body).returns(successResponse); @@ -67,6 +72,8 @@ describe.skip('request lib', () => { useRequest = useRequestUnbound.bind(null, httpClient); }); + */ + describe('sendRequest function', () => { it('uses the provided path, method, and body to send the request', async () => { const response = await sendRequest({ ...successRequest }); diff --git a/src/plugins/kibana_react/public/code_editor/README.md b/src/plugins/kibana_react/public/code_editor/README.md index 887a9c9990915..811038b58c828 100644 --- a/src/plugins/kibana_react/public/code_editor/README.md +++ b/src/plugins/kibana_react/public/code_editor/README.md @@ -8,6 +8,9 @@ This editor component allows easy access to: * Function signature widget * [Hover widget](https://microsoft.github.io/monaco-editor/playground.html#extending-language-services-hover-provider-example) -[_TODO: Examples of each_](https://github.com/elastic/kibana/issues/43812) +The Monaco editor doesn't automatically resize the editor area on window or container resize so this component includes a [resize detector](https://github.com/maslianok/react-resize-detector) to cause the Monaco editor to re-layout and adjust its size when the window or container size changes -The Monaco editor doesn't automatically resize the editor area on window or container resize so this component includes a [resize detector](https://github.com/maslianok/react-resize-detector) to cause the Monaco editor to re-layout and adjust its size when the window or container size changes \ No newline at end of file +## Storybook Examples +To run the CodeEditor storybook, from the root kibana directory, run `yarn storybook codeeditor` + +All stories for the component live in `code_editor.examples.tsx` \ No newline at end of file diff --git a/src/plugins/kibana_react/public/code_editor/code_editor.examples.tsx b/src/plugins/kibana_react/public/code_editor/code_editor.examples.tsx new file mode 100644 index 0000000000000..b6d5f2c5460f6 --- /dev/null +++ b/src/plugins/kibana_react/public/code_editor/code_editor.examples.tsx @@ -0,0 +1,235 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { action } from '@storybook/addon-actions'; +import { storiesOf } from '@storybook/react'; +import React from 'react'; +import { monaco as monacoEditor } from '@kbn/ui-shared-deps/monaco'; +import { CodeEditor } from './code_editor'; + +// A sample language definition with a few example tokens +// Taken from https://microsoft.github.io/monaco-editor/playground.html#extending-language-services-custom-languages +const simpleLogLang: monacoEditor.languages.IMonarchLanguage = { + tokenizer: { + root: [ + [/\[error.*/, 'constant'], + [/\[notice.*/, 'variable'], + [/\[info.*/, 'string'], + [/\[[a-zA-Z 0-9:]+\]/, 'tag'], + ], + }, +}; + +monacoEditor.languages.register({ id: 'loglang' }); +monacoEditor.languages.setMonarchTokensProvider('loglang', simpleLogLang); + +const logs = `[Sun Mar 7 20:54:27 2004] [notice] [client xx.xx.xx.xx] This is a notice! +[Sun Mar 7 20:58:27 2004] [info] [client xx.xx.xx.xx] (104)Connection reset by peer: client stopped connection before send body completed +[Sun Mar 7 21:16:17 2004] [error] [client xx.xx.xx.xx] File does not exist: /home/httpd/twiki/view/Main/WebHome +`; + +storiesOf('CodeEditor', module) + .addParameters({ + info: { + // CodeEditor has no PropTypes set so this table will show up + // as blank. I'm just disabling it to reduce confusion + propTablesExclude: [CodeEditor], + }, + }) + .add( + 'default', + () => ( +
+ +
+ ), + { + info: { + text: 'Plaintext Monaco Editor', + }, + } + ) + .add( + 'dark mode', + () => ( +
+ +
+ ), + { + info: { + text: 'The dark theme is automatically used when dark mode is enabled in Kibana', + }, + } + ) + .add( + 'custom log language', + () => ( +
+ +
+ ), + { + info: { + text: + 'Custom language example. Language definition taken from [here](https://microsoft.github.io/monaco-editor/playground.html#extending-language-services-custom-languages)', + }, + } + ) + .add( + 'hide minimap', + () => ( +
+ +
+ ), + { + info: { + text: 'The minimap (on left side of editor) can be disabled to save space', + }, + } + ) + .add( + 'suggestion provider', + () => { + const provideSuggestions = ( + model: monacoEditor.editor.ITextModel, + position: monacoEditor.Position, + context: monacoEditor.languages.CompletionContext + ) => { + const wordRange = new monacoEditor.Range( + position.lineNumber, + position.column, + position.lineNumber, + position.column + ); + + return { + suggestions: [ + { + label: 'Hello, World', + kind: monacoEditor.languages.CompletionItemKind.Variable, + documentation: { + value: '*Markdown* can be used in autocomplete help', + isTrusted: true, + }, + insertText: 'Hello, World', + range: wordRange, + }, + { + label: 'You know, for search', + kind: monacoEditor.languages.CompletionItemKind.Variable, + documentation: { value: 'Thanks `Monaco`', isTrusted: true }, + insertText: 'You know, for search', + range: wordRange, + }, + ], + }; + }; + + return ( +
+ +
+ ); + }, + { + info: { + text: 'Example suggestion provider is triggered by the `.` character', + }, + } + ) + .add( + 'hover provider', + () => { + const provideHover = ( + model: monacoEditor.editor.ITextModel, + position: monacoEditor.Position + ) => { + const word = model.getWordAtPosition(position); + + if (!word) { + return { + contents: [], + }; + } + + return { + contents: [ + { + value: `You're hovering over **${word.word}**`, + }, + ], + }; + }; + + return ( +
+ +
+ ); + }, + { + info: { + text: 'Hover dialog example can be triggered by hovering over a word', + }, + } + ); diff --git a/src/plugins/kibana_react/public/code_editor/scripts/storybook.ts b/src/plugins/kibana_react/public/code_editor/scripts/storybook.ts new file mode 100644 index 0000000000000..4fe7286987397 --- /dev/null +++ b/src/plugins/kibana_react/public/code_editor/scripts/storybook.ts @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { join } from 'path'; + +// eslint-disable-next-line +require('@kbn/storybook').runStorybookCli({ + name: 'code-editor', + storyGlobs: [join(__dirname, '..', '*.examples.tsx')], +}); diff --git a/src/plugins/vis_type_timeseries/server/routes/post_vis_schema.ts b/src/plugins/vis_type_timeseries/server/routes/post_vis_schema.ts index 7893ad456e83b..3127e03ada0ef 100644 --- a/src/plugins/vis_type_timeseries/server/routes/post_vis_schema.ts +++ b/src/plugins/vis_type_timeseries/server/routes/post_vis_schema.ts @@ -63,6 +63,7 @@ const backgroundColorRulesItems = Joi.object({ const gaugeColorRulesItems = Joi.object({ gauge: stringOptionalNullable, + text: stringOptionalNullable, id: stringOptionalNullable, operator: stringOptionalNullable, value: Joi.number(), @@ -95,6 +96,18 @@ const metricsItems = Joi.object({ }) ) .optional(), + percentiles: Joi.array() + .items( + Joi.object({ + id: stringRequired, + field: stringOptionalNullable, + mode: Joi.string().allow('line', 'band'), + shade: Joi.alternatives(numberOptional, stringOptionalNullable), + value: Joi.alternatives(numberOptional, stringOptionalNullable), + percentile: stringOptionalNullable, + }) + ) + .optional(), type: stringRequired, value: stringOptionalNullable, values: Joi.array() @@ -132,10 +145,13 @@ const seriesItems = Joi.object({ ) .optional(), fill: numberOptionalOrEmptyString, - filter: Joi.object({ - query: stringRequired, - language: stringOptionalNullable, - }).optional(), + filter: Joi.alternatives( + Joi.object({ + query: stringRequired, + language: stringOptionalNullable, + }).optional(), + Joi.string().valid('') + ), formatter: stringRequired, hide_in_legend: numberIntegerOptional, hidden: Joi.boolean().optional(), diff --git a/test/functional/page_objects/visualize_page.ts b/test/functional/page_objects/visualize_page.ts index 82ef3dc800f6c..3b63fa68d71ee 100644 --- a/test/functional/page_objects/visualize_page.ts +++ b/test/functional/page_objects/visualize_page.ts @@ -79,6 +79,10 @@ export function VisualizePageProvider({ getService, getPageObjects }: FtrProvide await this.waitForVisualizationSelectPage(); } + public async hasVisType(type: string) { + return await testSubjects.exists(`visType-${type}`); + } + public async clickVisType(type: string) { await testSubjects.click(`visType-${type}`); await header.waitUntilLoadingHasFinished(); @@ -100,6 +104,10 @@ export function VisualizePageProvider({ getService, getPageObjects }: FtrProvide await this.clickVisType('region_map'); } + public async hasRegionMap() { + return await this.hasVisType('region_map'); + } + public async clickMarkdownWidget() { await this.clickVisType('markdown'); } @@ -120,6 +128,10 @@ export function VisualizePageProvider({ getService, getPageObjects }: FtrProvide await this.clickVisType('tile_map'); } + public async hasTileMap() { + return await this.hasVisType('tile_map'); + } + public async clickTagCloud() { await this.clickVisType('tagcloud'); } @@ -144,6 +156,18 @@ export function VisualizePageProvider({ getService, getPageObjects }: FtrProvide await this.clickVisType('input_control_vis'); } + public async clickLensWidget() { + await this.clickVisType('lens'); + } + + public async clickMapsApp() { + await this.clickVisType('maps'); + } + + public async hasMapsApp() { + return await this.hasVisType('maps'); + } + public async createSimpleMarkdownViz(vizName: string) { await this.gotoVisualizationLandingPage(); await this.navigateToNewVisualization(); @@ -315,10 +339,6 @@ export function VisualizePageProvider({ getService, getPageObjects }: FtrProvide async () => (await globalNav.getLastBreadcrumb()) === vizName ); } - - public async clickLensWidget() { - await this.clickVisType('lens'); - } } return new VisualizePage(); diff --git a/x-pack/legacy/plugins/dashboard_mode/index.js b/x-pack/legacy/plugins/dashboard_mode/index.js index 94655adf981b4..ab90c6511de01 100644 --- a/x-pack/legacy/plugins/dashboard_mode/index.js +++ b/x-pack/legacy/plugins/dashboard_mode/index.js @@ -33,6 +33,15 @@ export function dashboardMode(kibana) { ), value: ['kibana_dashboard_only_user'], category: ['dashboard'], + deprecation: { + message: i18n.translate( + 'xpack.dashboardMode.uiSettings.dashboardsOnlyRolesDeprecation', + { + defaultMessage: 'This setting is deprecated and will be removed in Kibana 8.0.', + } + ), + docLinksKey: 'dashboardSettings', + }, }, }, app: { diff --git a/x-pack/legacy/plugins/ml/common/license/index.ts b/x-pack/legacy/plugins/ml/common/license/index.ts new file mode 100644 index 0000000000000..e901a9545897b --- /dev/null +++ b/x-pack/legacy/plugins/ml/common/license/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { MlLicense, LicenseStatus, MINIMUM_FULL_LICENSE, MINIMUM_LICENSE } from './ml_license'; diff --git a/x-pack/legacy/plugins/ml/common/license/ml_license.ts b/x-pack/legacy/plugins/ml/common/license/ml_license.ts new file mode 100644 index 0000000000000..8b631bf6ffb46 --- /dev/null +++ b/x-pack/legacy/plugins/ml/common/license/ml_license.ts @@ -0,0 +1,78 @@ +/* + * 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 { Observable, Subscription } from 'rxjs'; +import { ILicense, LICENSE_CHECK_STATE } from '../../../../../plugins/licensing/common/types'; +import { PLUGIN_ID } from '../constants/app'; + +export const MINIMUM_LICENSE = 'basic'; +export const MINIMUM_FULL_LICENSE = 'platinum'; + +export interface LicenseStatus { + isValid: boolean; + isSecurityEnabled: boolean; + message?: string; +} + +export class MlLicense { + private _licenseSubscription: Subscription | null = null; + private _license: ILicense | null = null; + private _isSecurityEnabled: boolean = false; + private _hasLicenseExpired: boolean = false; + private _isMlEnabled: boolean = false; + private _isMinimumLicense: boolean = false; + private _isFullLicense: boolean = false; + private _initialized: boolean = false; + + public setup( + license$: Observable, + postInitFunctions?: Array<(lic: MlLicense) => void> + ) { + this._licenseSubscription = license$.subscribe(async license => { + const { isEnabled: securityIsEnabled } = license.getFeature('security'); + + this._license = license; + this._isSecurityEnabled = securityIsEnabled; + this._hasLicenseExpired = this._license.status === 'expired'; + this._isMlEnabled = this._license.getFeature(PLUGIN_ID).isEnabled; + this._isMinimumLicense = + this._license.check(PLUGIN_ID, MINIMUM_LICENSE).state === LICENSE_CHECK_STATE.Valid; + this._isFullLicense = + this._license.check(PLUGIN_ID, MINIMUM_FULL_LICENSE).state === LICENSE_CHECK_STATE.Valid; + + if (this._initialized === false && postInitFunctions !== undefined) { + postInitFunctions.forEach(f => f(this)); + } + this._initialized = true; + }); + } + + public unsubscribe() { + if (this._licenseSubscription !== null) { + this._licenseSubscription.unsubscribe(); + } + } + + public isSecurityEnabled() { + return this._isSecurityEnabled; + } + + public hasLicenseExpired() { + return this._hasLicenseExpired; + } + + public isMlEnabled() { + return this._isMlEnabled; + } + + public isMinimumLicense() { + return this._isMinimumLicense; + } + + public isFullLicense() { + return this._isFullLicense; + } +} diff --git a/x-pack/legacy/plugins/ml/public/application/app.tsx b/x-pack/legacy/plugins/ml/public/application/app.tsx index 3acb24ac6e173..4c956bfabecc9 100644 --- a/x-pack/legacy/plugins/ml/public/application/app.tsx +++ b/x-pack/legacy/plugins/ml/public/application/app.tsx @@ -13,15 +13,18 @@ import { AppMountParameters, CoreStart } from 'kibana/public'; import { DataPublicPluginStart } from 'src/plugins/data/public'; import { SecurityPluginSetup } from '../../../../../plugins/security/public'; +import { LicensingPluginSetup } from '../../../../../plugins/licensing/public'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { setDependencyCache, clearCache } from './util/dependency_cache'; +import { setLicenseCache } from './license'; import { MlRouter } from './routing'; export interface MlDependencies extends AppMountParameters { data: DataPublicPluginStart; security: SecurityPluginSetup; + licensing: LicensingPluginSetup; __LEGACY: { XSRF: string; }; @@ -36,14 +39,14 @@ const App: FC = ({ coreStart, deps }) => { setDependencyCache({ indexPatterns: deps.data.indexPatterns, timefilter: deps.data.query.timefilter, + fieldFormats: deps.data.fieldFormats, + autocomplete: deps.data.autocomplete, config: coreStart.uiSettings!, chrome: coreStart.chrome!, docLinks: coreStart.docLinks!, toastNotifications: coreStart.notifications.toasts, overlays: coreStart.overlays, recentlyAccessed: coreStart.chrome!.recentlyAccessed, - fieldFormats: deps.data.fieldFormats, - autocomplete: deps.data.autocomplete, basePath: coreStart.http.basePath, savedObjectsClient: coreStart.savedObjects.client, XSRF: deps.__LEGACY.XSRF, @@ -51,7 +54,11 @@ const App: FC = ({ coreStart, deps }) => { http: coreStart.http, security: deps.security, }); + + const mlLicense = setLicenseCache(deps.licensing); + deps.onAppLeave(actions => { + mlLicense.unsubscribe(); clearCache(); return actions.default(); }); diff --git a/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table.test.js b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table.test.js index 206b9e01bab8c..b881bfe4f1fe6 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table.test.js +++ b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/anomalies_table.test.js @@ -11,7 +11,7 @@ import { getColumns } from './anomalies_table_columns'; jest.mock('../../privilege/check_privilege', () => ({ checkPermission: () => false, })); -jest.mock('../../license/check_license', () => ({ +jest.mock('../../license', () => ({ hasLicenseExpired: () => false, })); jest.mock('../../privilege/get_privileges', () => ({ diff --git a/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx index dce5e7ad52b09..695783883d02e 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx +++ b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx @@ -81,13 +81,18 @@ export const MainTabs: FC = ({ tabId, disableLinks }) => { return ( {tabs.map((tab: Tab) => { - const id = tab.id; + const { id, disabled } = tab; const testSubject = TAB_DATA[id].testSubject; const defaultPathId = TAB_DATA[id].pathId || id; // globalState (e.g. selected jobs and time range) should be retained when changing pages. // appState will not be considered. const fullGlobalStateString = globalState !== undefined ? `?_g=${encode(globalState)}` : ''; - return ( + + return disabled ? ( + + {tab.name} + + ) : ( = ({ tabId, disableLinks }) => { className={'mlNavigationMenu__mainTab'} onClick={() => onSelectedTabChanged(id)} isSelected={id === selectedTabId} - disabled={tab.disabled} > {tab.name} diff --git a/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/navigation_menu.tsx b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/navigation_menu.tsx index e7ba57e25354e..6be2d18e59741 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/navigation_menu.tsx +++ b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/navigation_menu.tsx @@ -7,7 +7,7 @@ import React, { Fragment, FC } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; -import { isFullLicense } from '../../license/check_license'; +import { isFullLicense } from '../../license'; import { TopNav } from './top_nav'; import { MainTabs } from './main_tabs'; diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx index 0f56f78c708ee..254788c52a7a8 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx @@ -22,7 +22,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { isFullLicense } from '../license/check_license'; +import { isFullLicense } from '../license'; import { useTimefilter } from '../contexts/kibana'; import { NavigationMenu } from '../components/navigation_menu'; diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.tsx index debadba19051b..dddf64ce2cfd3 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.tsx @@ -9,7 +9,7 @@ import moment from 'moment'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, EuiCard, EuiIcon } from '@elastic/eui'; import { ml } from '../../../../services/ml_api_service'; -import { isFullLicense } from '../../../../license/check_license'; +import { isFullLicense } from '../../../../license'; import { checkPermission } from '../../../../privilege/check_privilege'; import { mlNodesAvailable } from '../../../../ml_nodes_check/check_ml_nodes'; import { useMlKibana } from '../../../../contexts/kibana'; diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/page.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/page.tsx index 84c07651d323d..fbf42ef62265c 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/page.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/page.tsx @@ -31,7 +31,7 @@ import { SavedSearchSavedObject } from '../../../../common/types/kibana'; import { NavigationMenu } from '../../components/navigation_menu'; import { ML_JOB_FIELD_TYPES } from '../../../../common/constants/field_types'; import { SEARCH_QUERY_LANGUAGE } from '../../../../common/constants/search'; -import { isFullLicense } from '../../license/check_license'; +import { isFullLicense } from '../../license'; import { checkPermission } from '../../privilege/check_privilege'; import { mlNodesAvailable } from '../../ml_nodes_check/check_ml_nodes'; import { FullTimeRangeSelector } from '../../components/full_time_range_selector'; diff --git a/x-pack/legacy/plugins/ml/public/application/license/__tests__/check_license.js b/x-pack/legacy/plugins/ml/public/application/license/__tests__/check_license.js deleted file mode 100644 index 9ce0ec04befb6..0000000000000 --- a/x-pack/legacy/plugins/ml/public/application/license/__tests__/check_license.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { xpackInfo } from '../../../../../xpack_main/public/services/xpack_info'; -import { LICENSE_STATUS_VALID } from '../../../../../../common/constants/license_status'; -import { xpackFeatureAvailable } from '../check_license'; - -const initialInfo = { - features: { - watcher: { - status: LICENSE_STATUS_VALID, - }, - }, -}; - -describe('ML - check license', () => { - describe('xpackFeatureAvailable', () => { - beforeEach(() => { - xpackInfo.setAll(initialInfo); - }); - - it('returns true for enabled feature', () => { - const result = xpackFeatureAvailable('watcher'); - expect(result).to.be(true); - }); - - it('returns false for disabled feature', () => { - const result = xpackFeatureAvailable('noSuchFeature'); - expect(result).to.be(false); - }); - }); -}); diff --git a/x-pack/legacy/plugins/ml/public/application/license/check_license.tsx b/x-pack/legacy/plugins/ml/public/application/license/check_license.tsx index 4af753ddb4d1f..be5b702742baa 100644 --- a/x-pack/legacy/plugins/ml/public/application/license/check_license.tsx +++ b/x-pack/legacy/plugins/ml/public/application/license/check_license.tsx @@ -4,126 +4,74 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { EuiCallOut } from '@elastic/eui'; -import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public'; -// @ts-ignore No declaration file for module -import { xpackInfo } from '../../../../xpack_main/public/services/xpack_info'; -import { LICENSE_TYPE } from '../../../common/constants/license'; -import { LICENSE_STATUS_VALID } from '../../../../../common/constants/license_status'; -import { getOverlays } from '../util/dependency_cache'; +import { LicensingPluginSetup } from '../../../../../../plugins/licensing/public'; +import { MlClientLicense } from './ml_client_license'; -let licenseHasExpired = true; -let licenseType: LICENSE_TYPE | null = null; -let expiredLicenseBannerId: string; +let mlLicense: MlClientLicense | null = null; -export function checkFullLicense() { - const features = getFeatures(); - licenseType = features.licenseType; - - if (features.isAvailable === false) { - // ML is not enabled - return redirectToKibana(); - } else if (features.licenseType === LICENSE_TYPE.BASIC) { - // ML is enabled, but only with a basic or gold license - return redirectToBasic(); - } else { - // ML is enabled - setLicenseExpired(features); - return Promise.resolve(features); - } +/** + * Create a new mlLicense and cache it for later checks + * + * @export + * @param {LicensingPluginSetup} licensingSetup + * @returns {MlClientLicense} + */ +export function setLicenseCache(licensingSetup: LicensingPluginSetup) { + mlLicense = new MlClientLicense(); + mlLicense.setup(licensingSetup.license$); + return mlLicense; } -export function checkBasicLicense() { - const features = getFeatures(); - licenseType = features.licenseType; - - if (features.isAvailable === false) { - // ML is not enabled - return redirectToKibana(); - } else { - // ML is enabled - setLicenseExpired(features); - return Promise.resolve(features); +/** + * Used as routing resolver to stop the loading of a page if the current license + * is a trial, platinum or enterprise. + * + * @export + * @returns {Promise} Promise which resolves if the license is trial, platinum or enterprise and rejects if it isn't. + */ +export async function checkFullLicense() { + if (mlLicense === null) { + // this should never happen + console.error('ML Licensing not initialized'); // eslint-disable-line + return Promise.reject(); } -} -// a wrapper for checkFullLicense which doesn't resolve if the license has expired. -// this is used by all create jobs pages to redirect back to the jobs list -// if the user's license has expired. -export function checkLicenseExpired() { - return checkFullLicense() - .then((features: any) => { - if (features.hasExpired) { - window.location.href = '#/jobs'; - return Promise.reject(); - } else { - return Promise.resolve(features); - } - }) - .catch(() => { - return Promise.reject(); - }); + return mlLicense.fullLicenseResolver(); } -function setLicenseExpired(features: any) { - licenseHasExpired = features.hasExpired || false; - // If the license has expired ML app will still work for 7 days and then - // the job management endpoints (e.g. create job, start datafeed) will be restricted. - // Therefore we need to keep the app enabled but show an info banner to the user. - if (licenseHasExpired) { - const message = features.message; - if (expiredLicenseBannerId === undefined) { - // Only show the banner once with no way to dismiss it - const overlays = getOverlays(); - expiredLicenseBannerId = overlays.banners.add( - toMountPoint() - ); - } +/** + * Used as routing resolver to stop the loading of a page if the current license + * is at least basic. + * + * @export + * @returns {Promise} Promise resolves if the license is at least basic and rejects if it isn't. + */ +export async function checkBasicLicense() { + if (mlLicense === null) { + // this should never happen + console.error('ML Licensing not initialized'); // eslint-disable-line + return Promise.reject(); } -} -// Temporary hack for cutting over server to NP -function getFeatures() { - return { - isAvailable: true, - showLinks: true, - enableLinks: true, - licenseType: 1, - hasExpired: false, - }; - // return xpackInfo.get('features.ml'); -} - -function redirectToKibana() { - window.location.href = '/'; - return Promise.reject(); -} -function redirectToBasic() { - window.location.href = '#/datavisualizer'; - return Promise.reject(); + return mlLicense.basicLicenseResolver(); } +/** + * Check to see if the current license has expired + * + * @export + * @returns {boolean} + */ export function hasLicenseExpired() { - return licenseHasExpired; + return mlLicense !== null && mlLicense.hasLicenseExpired(); } +/** + * Check to see if the current license is trial, platinum or enterprise. + * + * @export + * @returns {boolean} + */ export function isFullLicense() { - return licenseType === LICENSE_TYPE.FULL; -} - -export function xpackFeatureAvailable(feature: string) { - // each plugin can register their own set of features. - // so we need specific checks for each one. - // this list can grow if we need to check other plugin's features. - switch (feature) { - case 'watcher': - // watcher only has a license status feature - // if watcher is disabled in kibana.yml, the feature is completely missing from xpackInfo - return xpackInfo.get(`features.${feature}.status`, false) === LICENSE_STATUS_VALID; - default: - // historically plugins have used `isAvailable` as a catch all for - // license and feature enabled checks - return xpackInfo.get(`features.${feature}.isAvailable`, false); - } + return mlLicense !== null && mlLicense.isFullLicense(); } diff --git a/x-pack/legacy/plugins/ml/public/application/license/expired_warning.tsx b/x-pack/legacy/plugins/ml/public/application/license/expired_warning.tsx new file mode 100644 index 0000000000000..22cb3260d6969 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/license/expired_warning.tsx @@ -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 React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiCallOut } from '@elastic/eui'; +import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public'; +import { getOverlays } from '../util/dependency_cache'; + +let expiredLicenseBannerId: string; + +export function showExpiredLicenseWarning() { + if (expiredLicenseBannerId === undefined) { + const message = i18n.translate('xpack.ml.checkLicense.licenseHasExpiredMessage', { + defaultMessage: 'Your Machine Learning license has expired.', + }); + // Only show the banner once with no way to dismiss it + const overlays = getOverlays(); + expiredLicenseBannerId = overlays.banners.add( + toMountPoint() + ); + } +} diff --git a/x-pack/legacy/plugins/ml/public/application/license/index.ts b/x-pack/legacy/plugins/ml/public/application/license/index.ts new file mode 100644 index 0000000000000..0b6866d52d070 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/license/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. + */ + +export { + checkBasicLicense, + checkFullLicense, + hasLicenseExpired, + isFullLicense, + setLicenseCache, +} from './check_license'; diff --git a/x-pack/legacy/plugins/ml/public/application/license/ml_client_license.ts b/x-pack/legacy/plugins/ml/public/application/license/ml_client_license.ts new file mode 100644 index 0000000000000..13809e15135e8 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/license/ml_client_license.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MlLicense } from '../../../common/license'; +import { showExpiredLicenseWarning } from './expired_warning'; + +export class MlClientLicense extends MlLicense { + fullLicenseResolver() { + if (this.isMlEnabled() === false || this.isMinimumLicense() === false) { + // ML is not enabled or the license isn't at least basic + return redirectToKibana(); + } + + if (this.isFullLicense() === false) { + // ML is enabled, but only with a basic or gold license + return redirectToBasic(); + } + + // ML is enabled + if (this.hasLicenseExpired()) { + showExpiredLicenseWarning(); + } + return Promise.resolve(); + } + + basicLicenseResolver() { + if (this.isMlEnabled() === false || this.isMinimumLicense() === false) { + // ML is not enabled or the license isn't at least basic + return redirectToKibana(); + } + + // ML is enabled + if (this.hasLicenseExpired()) { + showExpiredLicenseWarning(); + } + return Promise.resolve(); + } +} + +function redirectToKibana() { + window.location.href = '/'; + return Promise.reject(); +} + +function redirectToBasic() { + window.location.href = '#/datavisualizer'; + return Promise.reject(); +} diff --git a/x-pack/legacy/plugins/ml/public/application/management/index.ts b/x-pack/legacy/plugins/ml/public/application/management/index.ts index a05de8b0d0880..16bb3ddfd1c9b 100644 --- a/x-pack/legacy/plugins/ml/public/application/management/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/management/index.ts @@ -10,21 +10,36 @@ * you may not use this file except in compliance with the Elastic License. */ +import { npSetup } from 'ui/new_platform'; import { management } from 'ui/management'; import { i18n } from '@kbn/i18n'; import chrome from 'ui/chrome'; import { metadata } from 'ui/metadata'; -// @ts-ignore No declaration file for module -import { xpackInfo } from '../../../../xpack_main/public/services/xpack_info'; import { JOBS_LIST_PATH } from './management_urls'; -import { LICENSE_TYPE } from '../../../common/constants/license'; import { setDependencyCache } from '../util/dependency_cache'; import './jobs_list'; +import { + LicensingPluginSetup, + LICENSE_CHECK_STATE, +} from '../../../../../../plugins/licensing/public'; +import { PLUGIN_ID } from '../../../common/constants/app'; +import { MINIMUM_FULL_LICENSE } from '../../../common/license'; -if ( - xpackInfo.get('features.ml.showLinks', false) === true && - xpackInfo.get('features.ml.licenseType') === LICENSE_TYPE.FULL -) { +type PluginsSetupExtended = typeof npSetup.plugins & { + // adds licensing which isn't in the PluginsSetup interface, but does exist + licensing: LicensingPluginSetup; +}; + +const plugins = npSetup.plugins as PluginsSetupExtended; +const licencingSubscription = plugins.licensing.license$.subscribe(license => { + if (license.check(PLUGIN_ID, MINIMUM_FULL_LICENSE).state === LICENSE_CHECK_STATE.Valid) { + initManagementSection(); + // unsubscribe, we only want to register the plugin once. + licencingSubscription.unsubscribe(); + } +}); + +function initManagementSection() { const legacyBasePath = { prepend: chrome.addBasePath, get: chrome.getBasePath, diff --git a/x-pack/legacy/plugins/ml/public/application/privilege/check_privilege.ts b/x-pack/legacy/plugins/ml/public/application/privilege/check_privilege.ts index 6cc06231a08d0..ec9695a2ce668 100644 --- a/x-pack/legacy/plugins/ml/public/application/privilege/check_privilege.ts +++ b/x-pack/legacy/plugins/ml/public/application/privilege/check_privilege.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; -import { hasLicenseExpired } from '../license/check_license'; +import { hasLicenseExpired } from '../license'; import { Privileges, getDefaultPrivileges } from '../../../common/types/privileges'; import { getPrivileges, getManageMlPrivileges } from './get_privileges'; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/resolvers.ts b/x-pack/legacy/plugins/ml/public/application/routing/resolvers.ts index 5fc1ea533e87f..acaf3f3acd0c8 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/resolvers.ts +++ b/x-pack/legacy/plugins/ml/public/application/routing/resolvers.ts @@ -5,7 +5,7 @@ */ import { loadIndexPatterns, loadSavedSearches } from '../util/index_utils'; -import { checkFullLicense } from '../license/check_license'; +import { checkFullLicense } from '../license'; import { checkGetJobsPrivilege } from '../privilege/check_privilege'; import { getMlNodeCount } from '../ml_nodes_check/check_ml_nodes'; import { loadMlServerInfo } from '../services/ml_server_info'; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx index e89834018f5e6..d257a9c080c35 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx @@ -15,7 +15,7 @@ import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { DatavisualizerSelector } from '../../../datavisualizer'; -import { checkBasicLicense } from '../../../license/check_license'; +import { checkBasicLicense } from '../../../license'; import { checkFindFileStructurePrivilege } from '../../../privilege/check_privilege'; import { DATA_VISUALIZER_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx index b4ccccd0776eb..174b3e3b4b338 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx @@ -16,11 +16,10 @@ import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { FileDataVisualizerPage } from '../../../datavisualizer/file_based'; -import { checkBasicLicense } from '../../../license/check_license'; +import { checkBasicLicense } from '../../../license'; import { checkFindFileStructurePrivilege } from '../../../privilege/check_privilege'; import { loadIndexPatterns } from '../../../util/index_utils'; -import { getMlNodeCount } from '../../../ml_nodes_check'; import { DATA_VISUALIZER_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; const breadcrumbs = [ @@ -45,7 +44,6 @@ const PageWrapper: FC = ({ location, deps }) => { checkBasicLicense, loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns), checkFindFileStructurePrivilege, - getMlNodeCount, }); return ( diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx index 74ab916cb443f..a3dbc9f97124c 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx @@ -11,7 +11,7 @@ import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { Page } from '../../../datavisualizer/index_based'; -import { checkBasicLicense } from '../../../license/check_license'; +import { checkBasicLicense } from '../../../license'; import { checkGetJobsPrivilege } from '../../../privilege/check_privilege'; import { loadIndexPatterns } from '../../../util/index_utils'; import { checkMlNodesAvailable } from '../../../ml_nodes_check'; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx index ae35d783517d3..9411b415e4e4d 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx @@ -11,7 +11,7 @@ import { useResolver } from '../../use_resolver'; import { basicResolvers } from '../../resolvers'; import { Page, preConfiguredJobRedirect } from '../../../jobs/new_job/pages/index_or_search'; import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; -import { checkBasicLicense } from '../../../license/check_license'; +import { checkBasicLicense } from '../../../license'; import { loadIndexPatterns } from '../../../util/index_utils'; import { checkGetJobsPrivilege } from '../../../privilege/check_privilege'; import { checkMlNodesAvailable } from '../../../ml_nodes_check'; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/overview.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/overview.tsx index b1e00158efb94..ccb99985cb70c 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/overview.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/overview.tsx @@ -12,7 +12,7 @@ import { MlRoute, PageLoader, PageProps } from '../router'; import { useResolver } from '../use_resolver'; import { OverviewPage } from '../../overview'; -import { checkFullLicense } from '../../license/check_license'; +import { checkFullLicense } from '../../license'; import { checkGetJobsPrivilege } from '../../privilege/check_privilege'; import { getMlNodeCount } from '../../ml_nodes_check'; import { loadMlServerInfo } from '../../services/ml_server_info'; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx index c1bfaa2fe6c1e..9d5c4e9c0b0a0 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx @@ -16,7 +16,7 @@ import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { useTimefilter } from '../../../contexts/kibana'; -import { checkFullLicense } from '../../../license/check_license'; +import { checkFullLicense } from '../../../license'; import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; import { CalendarsList } from '../../../settings/calendars'; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx index 7af2e49e3a69e..bf039e3bd2354 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx @@ -16,7 +16,7 @@ import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { useTimefilter } from '../../../contexts/kibana'; -import { checkFullLicense } from '../../../license/check_license'; +import { checkFullLicense } from '../../../license'; import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; import { checkMlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes'; import { NewCalendar } from '../../../settings/calendars'; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list.tsx index 9c5c06b76247c..6839ad833cb06 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list.tsx @@ -16,7 +16,7 @@ import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { useTimefilter } from '../../../contexts/kibana'; -import { checkFullLicense } from '../../../license/check_license'; +import { checkFullLicense } from '../../../license'; import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; import { FilterLists } from '../../../settings/filter_lists'; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx index 752b889490e58..7b8bd6c3c81ac 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx @@ -16,7 +16,7 @@ import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { useTimefilter } from '../../../contexts/kibana'; -import { checkFullLicense } from '../../../license/check_license'; +import { checkFullLicense } from '../../../license'; import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; import { checkMlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes'; import { EditFilterList } from '../../../settings/filter_lists'; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/settings.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/settings.tsx index 10efb2dcc60c7..10ccc0987fe5d 100644 --- a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/settings.tsx +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/settings.tsx @@ -15,7 +15,7 @@ import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { useTimefilter } from '../../../contexts/kibana'; -import { checkFullLicense } from '../../../license/check_license'; +import { checkFullLicense } from '../../../license'; import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; import { Settings } from '../../../settings'; diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js index 8dc174040f9c8..5f61ccf47e9d7 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js @@ -10,7 +10,7 @@ jest.mock('../../../components/navigation_menu', () => ({ jest.mock('../../../privilege/check_privilege', () => ({ checkPermission: () => true, })); -jest.mock('../../../license/check_license', () => ({ +jest.mock('../../../license', () => ({ hasLicenseExpired: () => false, isFullLicense: () => false, })); diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/calendars_list.test.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/calendars_list.test.js index 677703bceeca7..3ea8e0c39fbb2 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/calendars_list.test.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/calendars_list.test.js @@ -16,7 +16,7 @@ jest.mock('../../../components/navigation_menu', () => ({ jest.mock('../../../privilege/check_privilege', () => ({ checkPermission: () => true, })); -jest.mock('../../../license/check_license', () => ({ +jest.mock('../../../license', () => ({ hasLicenseExpired: () => false, isFullLicense: () => false, })); diff --git a/x-pack/legacy/plugins/ml/public/application/util/dependency_cache.ts b/x-pack/legacy/plugins/ml/public/application/util/dependency_cache.ts index 6d1dfa96ca03e..c167d7e7c3d42 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/dependency_cache.ts +++ b/x-pack/legacy/plugins/ml/public/application/util/dependency_cache.ts @@ -76,6 +76,7 @@ export function setDependencyCache(deps: Partial) { cache.XSRF = deps.XSRF || null; cache.application = deps.application || null; cache.http = deps.http || null; + cache.security = deps.security || null; } export function getTimefilter() { diff --git a/x-pack/legacy/plugins/ml/public/legacy.ts b/x-pack/legacy/plugins/ml/public/legacy.ts index 7dfcf6a99c213..0c6c0bd8dd29e 100644 --- a/x-pack/legacy/plugins/ml/public/legacy.ts +++ b/x-pack/legacy/plugins/ml/public/legacy.ts @@ -8,14 +8,24 @@ import chrome from 'ui/chrome'; import { npSetup, npStart } from 'ui/new_platform'; import { PluginInitializerContext } from 'src/core/public'; import { SecurityPluginSetup } from '../../../../plugins/security/public'; +import { LicensingPluginSetup } from '../../../../plugins/licensing/public'; import { plugin } from '.'; const pluginInstance = plugin({} as PluginInitializerContext); +type PluginsSetupExtended = typeof npSetup.plugins & { + // adds plugins which aren't in the PluginsSetup interface, but do exist + security: SecurityPluginSetup; + licensing: LicensingPluginSetup; +}; + +const setupDependencies = npSetup.plugins as PluginsSetupExtended; + export const setup = pluginInstance.setup(npSetup.core, { data: npStart.plugins.data, - security: ((npSetup.plugins as unknown) as { security: SecurityPluginSetup }).security, // security isn't in the PluginsSetup interface, but does exist + security: setupDependencies.security, + licensing: setupDependencies.licensing, __LEGACY: { XSRF: chrome.getXsrfToken(), }, diff --git a/x-pack/legacy/plugins/ml/public/plugin.ts b/x-pack/legacy/plugins/ml/public/plugin.ts index 1061bb1b6b62b..c0369a74c070a 100644 --- a/x-pack/legacy/plugins/ml/public/plugin.ts +++ b/x-pack/legacy/plugins/ml/public/plugin.ts @@ -8,7 +8,7 @@ import { Plugin, CoreStart, CoreSetup } from 'src/core/public'; import { MlDependencies } from './application/app'; export class MlPlugin implements Plugin { - setup(core: CoreSetup, { data, security, __LEGACY }: MlDependencies) { + setup(core: CoreSetup, { data, security, licensing, __LEGACY }: MlDependencies) { core.application.register({ id: 'ml', title: 'Machine learning', @@ -23,6 +23,7 @@ export class MlPlugin implements Plugin { data, __LEGACY, security, + licensing, }); }, }); diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx index d05d59d15802d..81b8b04ed6648 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx @@ -23,7 +23,7 @@ export interface UseRules { pagination: PaginationOptions; filterOptions: FilterOptions; refetchPrePackagedRulesStatus?: () => void; - dispatchRulesInReducer?: (rules: Rule[]) => void; + dispatchRulesInReducer?: (rules: Rule[], pagination: Partial) => void; } /** @@ -59,14 +59,18 @@ export const useRules = ({ if (isSubscribed) { setRules(fetchRulesResult); if (dispatchRulesInReducer != null) { - dispatchRulesInReducer(fetchRulesResult.data); + dispatchRulesInReducer(fetchRulesResult.data, { + page: fetchRulesResult.page, + perPage: fetchRulesResult.perPage, + total: fetchRulesResult.total, + }); } } } catch (error) { if (isSubscribed) { errorToToaster({ title: i18n.RULE_FETCH_FAILURE, error, dispatchToaster }); if (dispatchRulesInReducer != null) { - dispatchRulesInReducer([]); + dispatchRulesInReducer([], {}); } } } diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.test.tsx index c60933733587d..062d7967bf301 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { bucketRulesResponse } from './helpers'; +import { bucketRulesResponse, showRulesTable } from './helpers'; import { mockRule, mockRuleError } from './__mocks__/mock'; import uuid from 'uuid'; import { Rule, RuleError } from '../../../../containers/detection_engine/rules'; @@ -44,4 +44,46 @@ describe('AllRulesTable Helpers', () => { }); }); }); + + describe('showRulesTable', () => { + test('returns false when rulesCustomInstalled and rulesInstalled are null', () => { + const result = showRulesTable({ + rulesCustomInstalled: null, + rulesInstalled: null, + }); + expect(result).toBeFalsy(); + }); + + test('returns false when rulesCustomInstalled and rulesInstalled are 0', () => { + const result = showRulesTable({ + rulesCustomInstalled: 0, + rulesInstalled: 0, + }); + expect(result).toBeFalsy(); + }); + + test('returns false when both rulesCustomInstalled and rulesInstalled checks return false', () => { + const result = showRulesTable({ + rulesCustomInstalled: 0, + rulesInstalled: null, + }); + expect(result).toBeFalsy(); + }); + + test('returns true if rulesCustomInstalled is not null or 0', () => { + const result = showRulesTable({ + rulesCustomInstalled: 5, + rulesInstalled: null, + }); + expect(result).toBeTruthy(); + }); + + test('returns true if rulesInstalled is not null or 0', () => { + const result = showRulesTable({ + rulesCustomInstalled: null, + rulesInstalled: 5, + }); + expect(result).toBeTruthy(); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx index 79fec526faf48..9676b83a26f55 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/index.tsx @@ -21,6 +21,7 @@ import { CreatePreBuiltRules, FilterOptions, Rule, + PaginationOptions, } from '../../../../containers/detection_engine/rules'; import { HeaderSection } from '../../../../components/header_section'; import { @@ -118,10 +119,11 @@ export const AllRules = React.memo( const history = useHistory(); const [, dispatchToaster] = useStateToaster(); - const setRules = useCallback((newRules: Rule[]) => { + const setRules = useCallback((newRules: Rule[], newPagination: Partial) => { dispatch({ type: 'setRules', rules: newRules, + pagination: newPagination, }); }, []); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/reducer.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/reducer.ts index 54da43efd66d9..0a4d169d13154 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/reducer.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/reducer.ts @@ -26,9 +26,8 @@ export type Action = | { type: 'exportRuleIds'; ids: string[] } | { type: 'loadingRuleIds'; ids: string[]; actionType: LoadingRuleAction } | { type: 'selectedRuleIds'; ids: string[] } - | { type: 'setRules'; rules: Rule[] } + | { type: 'setRules'; rules: Rule[]; pagination: Partial } | { type: 'updateRules'; rules: Rule[] } - | { type: 'updatePagination'; pagination: Partial } | { type: 'updateFilterOptions'; filterOptions: Partial; @@ -76,6 +75,10 @@ export const allRulesReducer = ( selectedRuleIds: [], loadingRuleIds: [], loadingRulesAction: null, + pagination: { + ...state.pagination, + ...action.pagination, + }, }; } case 'updateRules': { @@ -101,15 +104,6 @@ export const allRulesReducer = ( } return state; } - case 'updatePagination': { - return { - ...state, - pagination: { - ...state.pagination, - ...action.pagination, - }, - }; - } case 'updateFilterOptions': { return { ...state, diff --git a/x-pack/plugins/ingest_manager/server/integration_tests/router.test.ts b/x-pack/plugins/ingest_manager/server/integration_tests/router.test.ts index 3a13359cdf782..c2546454e2131 100644 --- a/x-pack/plugins/ingest_manager/server/integration_tests/router.test.ts +++ b/x-pack/plugins/ingest_manager/server/integration_tests/router.test.ts @@ -4,6 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +test.skip('requires one test', () => {}); + +/** + * skipped due to all being flaky: https://github.com/elastic/kibana/issues/58954 + * + * commented out due to hooks being called regardless of skip + * https://github.com/facebook/jest/issues/8379 + import { resolve } from 'path'; import * as kbnTestServer from '../../../../../src/test_utils/kbn_server'; @@ -23,8 +31,9 @@ function createXPackRoot(config: {} = {}) { } describe('ingestManager', () => { - describe.skip('default. manager, EPM, and Fleet all disabled', () => { + describe('default. manager, EPM, and Fleet all disabled', () => { let root: ReturnType; + beforeAll(async () => { root = createXPackRoot(); await root.setup(); @@ -50,8 +59,9 @@ describe('ingestManager', () => { }); }); - describe.skip('manager only (no EPM, no Fleet)', () => { + describe('manager only (no EPM, no Fleet)', () => { let root: ReturnType; + beforeAll(async () => { const ingestManagerConfig = { enabled: true, @@ -87,8 +97,9 @@ describe('ingestManager', () => { // https://github.com/jfsiii/kibana/blob/f73b54ebb7e0f6fc00efd8a6800a01eb2d9fb772/x-pack/plugins/ingest_manager/server/plugin.ts#L84 // adding tests to confirm the Fleet & EPM routes are never added - describe.skip('manager and EPM; no Fleet', () => { + describe('manager and EPM; no Fleet', () => { let root: ReturnType; + beforeAll(async () => { const ingestManagerConfig = { enabled: true, @@ -122,6 +133,7 @@ describe('ingestManager', () => { describe('manager and Fleet; no EPM)', () => { let root: ReturnType; + beforeAll(async () => { const ingestManagerConfig = { enabled: true, @@ -156,6 +168,7 @@ describe('ingestManager', () => { describe('all flags enabled: manager, EPM, and Fleet)', () => { let root: ReturnType; + beforeAll(async () => { const ingestManagerConfig = { enabled: true, @@ -188,3 +201,4 @@ describe('ingestManager', () => { }); }); }); +*/ diff --git a/x-pack/plugins/ml/server/lib/check_license/check_license.test.ts b/x-pack/plugins/ml/server/lib/check_license/check_license.test.ts deleted file mode 100644 index 942dbe3722617..0000000000000 --- a/x-pack/plugins/ml/server/lib/check_license/check_license.test.ts +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import sinon from 'sinon'; -import { set } from 'lodash'; -import { LicenseCheckResult } from '../../types'; -import { checkLicense } from './check_license'; - -describe('check_license', () => { - let mockLicenseInfo: LicenseCheckResult; - beforeEach(() => (mockLicenseInfo = {} as LicenseCheckResult)); - - describe('license information is undefined', () => { - it('should set isAvailable to false', () => { - expect(checkLicense(undefined as any).isAvailable).to.be(false); - }); - - it('should set showLinks to true', () => { - expect(checkLicense(undefined as any).showLinks).to.be(true); - }); - - it('should set enableLinks to false', () => { - expect(checkLicense(undefined as any).enableLinks).to.be(false); - }); - - it('should set a message', () => { - expect(checkLicense(undefined as any).message).to.not.be(undefined); - }); - }); - - describe('license information is not available', () => { - beforeEach(() => { - mockLicenseInfo.isAvailable = false; - }); - - it('should set isAvailable to false', () => { - expect(checkLicense(mockLicenseInfo).isAvailable).to.be(false); - }); - - it('should set showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).showLinks).to.be(true); - }); - - it('should set enableLinks to false', () => { - expect(checkLicense(mockLicenseInfo).enableLinks).to.be(false); - }); - - it('should set a message', () => { - expect(checkLicense(mockLicenseInfo).message).to.not.be(undefined); - }); - }); - - describe('license information is available', () => { - beforeEach(() => { - mockLicenseInfo.isAvailable = true; - mockLicenseInfo.type = 'basic'; - }); - - describe('& ML is disabled in Elasticsearch', () => { - beforeEach(() => { - set( - mockLicenseInfo, - 'feature', - sinon - .stub() - .withArgs('ml') - .returns({ isEnabled: false }) - ); - }); - - it('should set showLinks to false', () => { - expect(checkLicense(mockLicenseInfo).showLinks).to.be(false); - }); - - it('should set isAvailable to false', () => { - expect(checkLicense(mockLicenseInfo).isAvailable).to.be(false); - }); - - it('should set enableLinks to false', () => { - expect(checkLicense(mockLicenseInfo).enableLinks).to.be(false); - }); - - it('should set a message', () => { - expect(checkLicense(mockLicenseInfo).message).to.not.be(undefined); - }); - }); - - describe('& ML is enabled in Elasticsearch', () => { - beforeEach(() => { - mockLicenseInfo.isEnabled = true; - }); - - describe('& license is >= platinum', () => { - beforeEach(() => { - mockLicenseInfo.type = 'platinum'; - }); - describe('& license is active', () => { - beforeEach(() => { - mockLicenseInfo.isActive = true; - }); - - it('should set isAvailable to true', () => { - expect(checkLicense(mockLicenseInfo).isAvailable).to.be(true); - }); - - it('should set showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).showLinks).to.be(true); - }); - - it('should set enableLinks to true', () => { - expect(checkLicense(mockLicenseInfo).enableLinks).to.be(true); - }); - - it('should not set a message', () => { - expect(checkLicense(mockLicenseInfo).message).to.be(undefined); - }); - }); - - describe('& license is expired', () => { - beforeEach(() => { - mockLicenseInfo.isActive = false; - }); - - it('should set isAvailable to true', () => { - expect(checkLicense(mockLicenseInfo).isAvailable).to.be(true); - }); - - it('should set showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).showLinks).to.be(true); - }); - - it('should set enableLinks to true', () => { - expect(checkLicense(mockLicenseInfo).enableLinks).to.be(true); - }); - - it('should set a message', () => { - expect(checkLicense(mockLicenseInfo).message).to.not.be(undefined); - }); - }); - }); - - describe('& license is basic', () => { - beforeEach(() => { - mockLicenseInfo.type = 'basic'; - }); - - describe('& license is active', () => { - beforeEach(() => { - mockLicenseInfo.isActive = true; - }); - - it('should set isAvailable to true', () => { - expect(checkLicense(mockLicenseInfo).isAvailable).to.be(true); - }); - - it('should set showLinks to true', () => { - expect(checkLicense(mockLicenseInfo).showLinks).to.be(true); - }); - }); - }); - }); - }); -}); diff --git a/x-pack/plugins/ml/server/lib/check_license/check_license.ts b/x-pack/plugins/ml/server/lib/check_license/check_license.ts deleted file mode 100644 index 5bf3d590a1912..0000000000000 --- a/x-pack/plugins/ml/server/lib/check_license/check_license.ts +++ /dev/null @@ -1,82 +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 { i18n } from '@kbn/i18n'; -import { - LICENSE_TYPE, - VALID_FULL_LICENSE_MODES, -} from '../../../../../legacy/plugins/ml/common/constants/license'; -import { LicenseCheckResult } from '../../types'; - -interface Response { - isAvailable: boolean; - showLinks: boolean; - enableLinks: boolean; - licenseType?: LICENSE_TYPE; - hasExpired?: boolean; - message?: string; -} - -export function checkLicense(licenseCheckResult: LicenseCheckResult): Response { - // If, for some reason, we cannot get the license information - // from Elasticsearch, assume worst case and disable the Machine Learning UI - if (licenseCheckResult === undefined || !licenseCheckResult.isAvailable) { - return { - isAvailable: false, - showLinks: true, - enableLinks: false, - message: i18n.translate( - 'xpack.ml.checkLicense.licenseInformationNotAvailableThisTimeMessage', - { - defaultMessage: - 'You cannot use Machine Learning because license information is not available at this time.', - } - ), - }; - } - - const featureEnabled = licenseCheckResult.isEnabled; - if (!featureEnabled) { - return { - isAvailable: false, - showLinks: false, - enableLinks: false, - message: i18n.translate('xpack.ml.checkLicense.mlIsUnavailableMessage', { - defaultMessage: 'Machine Learning is unavailable', - }), - }; - } - - const isLicenseModeValid = - licenseCheckResult.type && VALID_FULL_LICENSE_MODES.includes(licenseCheckResult.type); - const licenseType = isLicenseModeValid === true ? LICENSE_TYPE.FULL : LICENSE_TYPE.BASIC; - const isLicenseActive = licenseCheckResult.isActive; - const licenseTypeName = licenseCheckResult.type; - - // Platinum or trial license is valid but not active, i.e. expired - if (licenseType === LICENSE_TYPE.FULL && isLicenseActive === false) { - return { - isAvailable: true, - showLinks: true, - enableLinks: true, - hasExpired: true, - licenseType, - message: i18n.translate('xpack.ml.checkLicense.licenseHasExpiredMessage', { - defaultMessage: 'Your {licenseTypeName} Machine Learning license has expired.', - values: { licenseTypeName }, - }), - }; - } - - // License is valid and active - return { - isAvailable: true, - showLinks: true, - enableLinks: true, - licenseType, - hasExpired: false, - }; -} diff --git a/x-pack/plugins/ml/server/lib/check_privileges/check_privileges.test.ts b/x-pack/plugins/ml/server/lib/check_privileges/check_privileges.test.ts index 0690aa53576a5..4dd9100e1b67a 100644 --- a/x-pack/plugins/ml/server/lib/check_privileges/check_privileges.test.ts +++ b/x-pack/plugins/ml/server/lib/check_privileges/check_privileges.test.ts @@ -7,30 +7,27 @@ import { callWithRequestProvider } from './__mocks__/call_with_request'; import { privilegesProvider } from './check_privileges'; import { mlPrivileges } from './privileges'; +import { MlLicense } from '../../../../../legacy/plugins/ml/common/license'; -const licenseCheckResultWithSecurity = { - isAvailable: true, - isEnabled: true, - isSecurityDisabled: false, - type: 'platinum', - isActive: true, -}; +const mlLicenseWithSecurity = { + isSecurityEnabled: () => true, + isFullLicense: () => true, +} as MlLicense; -const licenseCheckResultWithOutSecurity = { - ...licenseCheckResultWithSecurity, - isSecurityDisabled: true, -}; +const mlLicenseWithOutSecurity = { + isSecurityEnabled: () => false, + isFullLicense: () => true, +} as MlLicense; -const licenseCheckResultWithOutSecurityBasicLicense = { - ...licenseCheckResultWithSecurity, - isSecurityDisabled: true, - type: 'basic', -}; +const mlLicenseWithOutSecurityBasicLicense = { + isSecurityEnabled: () => false, + isFullLicense: () => false, +} as MlLicense; -const licenseCheckResultWithSecurityBasicLicense = { - ...licenseCheckResultWithSecurity, - type: 'basic', -}; +const mlLicenseWithSecurityBasicLicense = { + isSecurityEnabled: () => true, + isFullLicense: () => false, +} as MlLicense; const mlIsEnabled = async () => true; const mlIsNotEnabled = async () => false; @@ -47,7 +44,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('partialPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - licenseCheckResultWithSecurity, + mlLicenseWithSecurity, mlIsEnabled ); const { capabilities } = await getPrivileges(); @@ -62,7 +59,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('partialPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - licenseCheckResultWithSecurity, + mlLicenseWithSecurity, mlIsEnabled ); const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); @@ -97,7 +94,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('fullPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - licenseCheckResultWithSecurity, + mlLicenseWithSecurity, mlIsEnabled ); const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); @@ -132,7 +129,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('upgradeWithFullPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - licenseCheckResultWithSecurity, + mlLicenseWithSecurity, mlIsEnabled ); const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); @@ -167,7 +164,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('upgradeWithPartialPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - licenseCheckResultWithSecurity, + mlLicenseWithSecurity, mlIsEnabled ); const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); @@ -202,7 +199,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('partialPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - licenseCheckResultWithSecurityBasicLicense, + mlLicenseWithSecurityBasicLicense, mlIsEnabled ); const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); @@ -237,7 +234,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('fullPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - licenseCheckResultWithSecurityBasicLicense, + mlLicenseWithSecurityBasicLicense, mlIsEnabled ); const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); @@ -272,7 +269,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('fullPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - licenseCheckResultWithSecurity, + mlLicenseWithSecurity, mlIsNotEnabled ); const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); @@ -309,7 +306,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('partialPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - licenseCheckResultWithOutSecurity, + mlLicenseWithOutSecurity, mlIsEnabled ); const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); @@ -344,7 +341,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('upgradeWithFullPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - licenseCheckResultWithOutSecurity, + mlLicenseWithOutSecurity, mlIsEnabled ); const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); @@ -379,7 +376,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('upgradeWithPartialPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - licenseCheckResultWithOutSecurity, + mlLicenseWithOutSecurity, mlIsEnabled ); const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); @@ -414,7 +411,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('partialPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - licenseCheckResultWithOutSecurityBasicLicense, + mlLicenseWithOutSecurityBasicLicense, mlIsEnabled ); const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); @@ -449,7 +446,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('fullPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - licenseCheckResultWithOutSecurityBasicLicense, + mlLicenseWithOutSecurityBasicLicense, mlIsEnabled ); const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); @@ -484,7 +481,7 @@ describe('check_privileges', () => { const callWithRequest = callWithRequestProvider('partialPrivileges'); const { getPrivileges } = privilegesProvider( callWithRequest, - licenseCheckResultWithOutSecurity, + mlLicenseWithOutSecurity, mlIsNotEnabled ); const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); diff --git a/x-pack/plugins/ml/server/lib/check_privileges/check_privileges.ts b/x-pack/plugins/ml/server/lib/check_privileges/check_privileges.ts index a427780d13344..f26040385b9f5 100644 --- a/x-pack/plugins/ml/server/lib/check_privileges/check_privileges.ts +++ b/x-pack/plugins/ml/server/lib/check_privileges/check_privileges.ts @@ -10,9 +10,7 @@ import { getDefaultPrivileges, } from '../../../../../legacy/plugins/ml/common/types/privileges'; import { upgradeCheckProvider } from './upgrade'; -import { checkLicense } from '../check_license'; -import { LICENSE_TYPE } from '../../../../../legacy/plugins/ml/common/constants/license'; -import { LicenseCheckResult } from '../../types'; +import { MlLicense } from '../../../../../legacy/plugins/ml/common/license'; import { mlPrivileges } from './privileges'; @@ -27,7 +25,7 @@ interface Response { export function privilegesProvider( callAsCurrentUser: IScopedClusterClient['callAsCurrentUser'], - licenseCheckResult: LicenseCheckResult, + mlLicense: MlLicense, isMlEnabledInSpace: () => Promise, ignoreSpaces: boolean = false ) { @@ -37,9 +35,9 @@ export function privilegesProvider( const privileges = getDefaultPrivileges(); const upgradeInProgress = await isUpgradeInProgress(); - const securityDisabled = licenseCheckResult.isSecurityDisabled; - const license = checkLicense(licenseCheckResult); - const isPlatinumOrTrialLicense = license.licenseType === LICENSE_TYPE.FULL; + const isSecurityEnabled = mlLicense.isSecurityEnabled(); + + const isPlatinumOrTrialLicense = mlLicense.isFullLicense(); const mlFeatureEnabledInSpace = await isMlEnabledInSpace(); const setGettingPrivileges = isPlatinumOrTrialLicense @@ -61,7 +59,7 @@ export function privilegesProvider( }; } - if (securityDisabled === true) { + if (isSecurityEnabled === false) { if (upgradeInProgress === true) { // if security is disabled and an upgrade in is progress, // force all "getting" privileges to be true diff --git a/x-pack/plugins/ml/server/lib/license/index.ts b/x-pack/plugins/ml/server/lib/license/index.ts new file mode 100644 index 0000000000000..9c4271b65b00d --- /dev/null +++ b/x-pack/plugins/ml/server/lib/license/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { MlServerLicense } from './ml_server_license'; diff --git a/x-pack/plugins/ml/server/lib/license/ml_server_license.ts b/x-pack/plugins/ml/server/lib/license/ml_server_license.ts new file mode 100644 index 0000000000000..7602ab4919e81 --- /dev/null +++ b/x-pack/plugins/ml/server/lib/license/ml_server_license.ts @@ -0,0 +1,35 @@ +/* + * 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 { + KibanaRequest, + KibanaResponseFactory, + RequestHandler, + RequestHandlerContext, +} from 'src/core/server'; + +import { MlLicense } from '../../../../../legacy/plugins/ml/common/license'; + +export class MlServerLicense extends MlLicense { + public fullLicenseAPIGuard(handler: RequestHandler) { + return guard(() => this.isFullLicense(), handler); + } + public basicLicenseAPIGuard(handler: RequestHandler) { + return guard(() => this.isMinimumLicense(), handler); + } +} + +function guard(check: () => boolean, handler: RequestHandler) { + return ( + context: RequestHandlerContext, + request: KibanaRequest, + response: KibanaResponseFactory + ) => { + if (check() === false) { + return response.forbidden(); + } + return handler(context, request, response); + }; +} diff --git a/x-pack/plugins/ml/server/lib/sample_data_sets/index.ts b/x-pack/plugins/ml/server/lib/sample_data_sets/index.ts index c922c9eb7c029..50553cfa7b889 100644 --- a/x-pack/plugins/ml/server/lib/sample_data_sets/index.ts +++ b/x-pack/plugins/ml/server/lib/sample_data_sets/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { addLinksToSampleDatasets } from './sample_data_sets'; +export { initSampleDataSets } from './sample_data_sets'; diff --git a/x-pack/plugins/ml/server/lib/sample_data_sets/sample_data_sets.ts b/x-pack/plugins/ml/server/lib/sample_data_sets/sample_data_sets.ts index 2082538adfed1..3fd99051a2484 100644 --- a/x-pack/plugins/ml/server/lib/sample_data_sets/sample_data_sets.ts +++ b/x-pack/plugins/ml/server/lib/sample_data_sets/sample_data_sets.ts @@ -5,23 +5,32 @@ */ import { i18n } from '@kbn/i18n'; +import { MlLicense } from '../../../../../legacy/plugins/ml/common/license'; +import { PluginsSetup } from '../../types'; -export function addLinksToSampleDatasets(server: any) { - const sampleDataLinkLabel = i18n.translate('xpack.ml.sampleDataLinkLabel', { - defaultMessage: 'ML jobs', - }); +export function initSampleDataSets(mlLicense: MlLicense, plugins: PluginsSetup) { + if (mlLicense.isMlEnabled() && mlLicense.isFullLicense()) { + const sampleDataLinkLabel = i18n.translate('xpack.ml.sampleDataLinkLabel', { + defaultMessage: 'ML jobs', + }); + const { addAppLinksToSampleDataset } = plugins.home.sampleData; - server.addAppLinksToSampleDataset('ecommerce', { - path: - '/app/ml#/modules/check_view_or_create?id=sample_data_ecommerce&index=ff959d40-b880-11e8-a6d9-e546fe2bba5f', - label: sampleDataLinkLabel, - icon: 'machineLearningApp', - }); + addAppLinksToSampleDataset('ecommerce', [ + { + path: + '/app/ml#/modules/check_view_or_create?id=sample_data_ecommerce&index=ff959d40-b880-11e8-a6d9-e546fe2bba5f', + label: sampleDataLinkLabel, + icon: 'machineLearningApp', + }, + ]); - server.addAppLinksToSampleDataset('logs', { - path: - '/app/ml#/modules/check_view_or_create?id=sample_data_weblogs&index=90943e30-9a47-11e8-b64d-95841ca0b247', - label: sampleDataLinkLabel, - icon: 'machineLearningApp', - }); + addAppLinksToSampleDataset('logs', [ + { + path: + '/app/ml#/modules/check_view_or_create?id=sample_data_weblogs&index=90943e30-9a47-11e8-b64d-95841ca0b247', + label: sampleDataLinkLabel, + icon: 'machineLearningApp', + }, + ]); + } } diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts index b5adf1fedec79..a3f5733738432 100644 --- a/x-pack/plugins/ml/server/plugin.ts +++ b/x-pack/plugins/ml/server/plugin.ts @@ -6,15 +6,14 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, IScopedClusterClient, Logger, PluginInitializerContext } from 'src/core/server'; -import { LicenseCheckResult, PluginsSetup, RouteInitialization } from './types'; +import { PluginsSetup, RouteInitialization } from './types'; import { PLUGIN_ID } from '../../../legacy/plugins/ml/common/constants/app'; -import { VALID_FULL_LICENSE_MODES } from '../../../legacy/plugins/ml/common/constants/license'; // @ts-ignore: could not find declaration file for module import { elasticsearchJsPlugin } from './client/elasticsearch_ml'; import { makeMlUsageCollector } from './lib/ml_telemetry'; import { initMlServerLog } from './client/log'; -import { addLinksToSampleDatasets } from './lib/sample_data_sets'; +import { initSampleDataSets } from './lib/sample_data_sets'; import { annotationRoutes } from './routes/annotations'; import { calendars } from './routes/calendars'; @@ -33,6 +32,8 @@ import { jobValidationRoutes } from './routes/job_validation'; import { notificationRoutes } from './routes/notification_settings'; import { resultsServiceRoutes } from './routes/results_service'; import { systemRoutes } from './routes/system'; +import { MlLicense } from '../../../legacy/plugins/ml/common/license'; +import { MlServerLicense } from './lib/license'; declare module 'kibana/server' { interface RequestHandlerContext { @@ -43,25 +44,17 @@ declare module 'kibana/server' { } export class MlServerPlugin { - private readonly pluginId: string = PLUGIN_ID; private log: Logger; private version: string; - - private licenseCheckResults: LicenseCheckResult = { - isAvailable: false, - isActive: false, - isEnabled: false, - isSecurityDisabled: false, - }; + private mlLicense: MlServerLicense; constructor(ctx: PluginInitializerContext) { this.log = ctx.logger.get(); this.version = ctx.env.packageInfo.branch; + this.mlLicense = new MlServerLicense(); } public setup(coreSetup: CoreSetup, plugins: PluginsSetup) { - let sampleLinksInitialized = false; - plugins.features.registerFeature({ id: PLUGIN_ID, name: i18n.translate('xpack.ml.featureRegistry.mlFeatureName', { @@ -87,6 +80,10 @@ export class MlServerPlugin { }, }); + this.mlLicense.setup(plugins.licensing.license$, [ + (mlLicense: MlLicense) => initSampleDataSets(mlLicense, plugins), + ]); + // Can access via router's handler function 'context' parameter - context.ml.mlClient const mlClient = coreSetup.elasticsearch.createClient(PLUGIN_ID, { plugins: [elasticsearchJsPlugin], @@ -100,7 +97,7 @@ export class MlServerPlugin { const routeInit: RouteInitialization = { router: coreSetup.http.createRouter(), - getLicenseCheckResults: () => this.licenseCheckResults, + mlLicense: this.mlLicense, }; annotationRoutes(routeInit, plugins.security); @@ -127,42 +124,11 @@ export class MlServerPlugin { coreSetup.getStartServices().then(([core]) => { makeMlUsageCollector(plugins.usageCollection, core.savedObjects); }); - - plugins.licensing.license$.subscribe(async license => { - const { isEnabled: securityIsEnabled } = license.getFeature('security'); - // @ts-ignore isAvailable is not read - const { isAvailable, isEnabled } = license.getFeature(this.pluginId); - - this.licenseCheckResults = { - isActive: license.isActive, - // This `isAvailable` check for the ml plugin returns false for a basic license - // ML should be available on basic with reduced functionality (only file data visualizer) - // TODO: This will need to be updated in the second step of this cutover to NP. - isAvailable: isEnabled, - isEnabled, - isSecurityDisabled: securityIsEnabled === false, - type: license.type, - }; - - if (sampleLinksInitialized === false) { - sampleLinksInitialized = true; - // Add links to the Kibana sample data sets if ml is enabled - // and license is trial or platinum. - if (isEnabled === true && plugins.home) { - if ( - this.licenseCheckResults.type && - VALID_FULL_LICENSE_MODES.includes(this.licenseCheckResults.type) - ) { - addLinksToSampleDatasets({ - addAppLinksToSampleDataset: plugins.home.sampleData.addAppLinksToSampleDataset, - }); - } - } - } - }); } public start() {} - public stop() {} + public stop() { + this.mlLicense.unsubscribe(); + } } diff --git a/x-pack/plugins/ml/server/routes/annotations.ts b/x-pack/plugins/ml/server/routes/annotations.ts index bcc0238c366a3..16483bf8b887e 100644 --- a/x-pack/plugins/ml/server/routes/annotations.ts +++ b/x-pack/plugins/ml/server/routes/annotations.ts @@ -13,7 +13,6 @@ import { SecurityPluginSetup } from '../../../security/server'; import { isAnnotationsFeatureAvailable } from '../lib/check_annotations'; import { annotationServiceProvider } from '../models/annotation_service'; import { wrapError } from '../client/error_wrapper'; -import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; import { RouteInitialization } from '../types'; import { deleteAnnotationSchema, @@ -36,7 +35,7 @@ function getAnnotationsFeatureUnavailableErrorMessage() { * Routes for annotations */ export function annotationRoutes( - { router, getLicenseCheckResults }: RouteInitialization, + { router, mlLicense }: RouteInitialization, securityPlugin: SecurityPluginSetup ) { /** @@ -61,7 +60,7 @@ export function annotationRoutes( body: schema.object(getAnnotationsSchema), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { getAnnotations } = annotationServiceProvider(context); const resp = await getAnnotations(request.body); @@ -92,7 +91,7 @@ export function annotationRoutes( body: schema.object(indexAnnotationSchema), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const annotationsFeatureAvailable = await isAnnotationsFeatureAvailable( context.ml!.mlClient.callAsCurrentUser @@ -131,7 +130,7 @@ export function annotationRoutes( params: schema.object(deleteAnnotationSchema), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const annotationsFeatureAvailable = await isAnnotationsFeatureAvailable( context.ml!.mlClient.callAsCurrentUser diff --git a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts index 7bf2fb7bc6903..5e1ca72a7200d 100644 --- a/x-pack/plugins/ml/server/routes/anomaly_detectors.ts +++ b/x-pack/plugins/ml/server/routes/anomaly_detectors.ts @@ -6,7 +6,6 @@ import { schema } from '@kbn/config-schema'; import { wrapError } from '../client/error_wrapper'; -import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; import { RouteInitialization } from '../types'; import { anomalyDetectionJobSchema, @@ -16,7 +15,7 @@ import { /** * Routes for the anomaly detectors */ -export function jobRoutes({ router, getLicenseCheckResults }: RouteInitialization) { +export function jobRoutes({ router, mlLicense }: RouteInitialization) { /** * @apiGroup AnomalyDetectors * @@ -32,7 +31,7 @@ export function jobRoutes({ router, getLicenseCheckResults }: RouteInitializatio path: '/api/ml/anomaly_detectors', validate: false, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const results = await context.ml!.mlClient.callAsCurrentUser('ml.jobs'); return response.ok({ @@ -62,7 +61,7 @@ export function jobRoutes({ router, getLicenseCheckResults }: RouteInitializatio }), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { jobId } = request.params; const results = await context.ml!.mlClient.callAsCurrentUser('ml.jobs', { jobId }); @@ -90,7 +89,7 @@ export function jobRoutes({ router, getLicenseCheckResults }: RouteInitializatio path: '/api/ml/anomaly_detectors/_stats', validate: false, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const results = await context.ml!.mlClient.callAsCurrentUser('ml.jobStats'); return response.ok({ @@ -120,7 +119,7 @@ export function jobRoutes({ router, getLicenseCheckResults }: RouteInitializatio }), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { jobId } = request.params; const results = await context.ml!.mlClient.callAsCurrentUser('ml.jobStats', { jobId }); @@ -152,7 +151,7 @@ export function jobRoutes({ router, getLicenseCheckResults }: RouteInitializatio body: schema.object({ ...anomalyDetectionJobSchema }), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { jobId } = request.params; const results = await context.ml!.mlClient.callAsCurrentUser('ml.addJob', { @@ -187,7 +186,7 @@ export function jobRoutes({ router, getLicenseCheckResults }: RouteInitializatio body: schema.object({ ...anomalyDetectionUpdateJobSchema }), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { jobId } = request.params; const results = await context.ml!.mlClient.callAsCurrentUser('ml.updateJob', { @@ -221,7 +220,7 @@ export function jobRoutes({ router, getLicenseCheckResults }: RouteInitializatio }), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { jobId } = request.params; const results = await context.ml!.mlClient.callAsCurrentUser('ml.openJob', { @@ -254,7 +253,7 @@ export function jobRoutes({ router, getLicenseCheckResults }: RouteInitializatio }), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const options: { jobId: string; force?: boolean } = { jobId: request.params.jobId, @@ -291,7 +290,7 @@ export function jobRoutes({ router, getLicenseCheckResults }: RouteInitializatio }), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const options: { jobId: string; force?: boolean } = { jobId: request.params.jobId, @@ -326,7 +325,7 @@ export function jobRoutes({ router, getLicenseCheckResults }: RouteInitializatio body: schema.any(), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const results = await context.ml!.mlClient.callAsCurrentUser('ml.validateDetector', { body: request.body, @@ -359,7 +358,7 @@ export function jobRoutes({ router, getLicenseCheckResults }: RouteInitializatio body: schema.object({ duration: schema.any() }), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const jobId = request.params.jobId; const duration = request.body.duration; @@ -407,7 +406,7 @@ export function jobRoutes({ router, getLicenseCheckResults }: RouteInitializatio }), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const results = await context.ml!.mlClient.callAsCurrentUser('ml.records', { jobId: request.params.jobId, @@ -456,7 +455,7 @@ export function jobRoutes({ router, getLicenseCheckResults }: RouteInitializatio }), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const results = await context.ml!.mlClient.callAsCurrentUser('ml.buckets', { jobId: request.params.jobId, @@ -499,7 +498,7 @@ export function jobRoutes({ router, getLicenseCheckResults }: RouteInitializatio }), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const results = await context.ml!.mlClient.callAsCurrentUser('ml.overallBuckets', { jobId: request.params.jobId, @@ -537,7 +536,7 @@ export function jobRoutes({ router, getLicenseCheckResults }: RouteInitializatio }), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const options = { jobId: request.params.jobId, diff --git a/x-pack/plugins/ml/server/routes/calendars.ts b/x-pack/plugins/ml/server/routes/calendars.ts index ae494d3578890..5d1161e928d11 100644 --- a/x-pack/plugins/ml/server/routes/calendars.ts +++ b/x-pack/plugins/ml/server/routes/calendars.ts @@ -6,7 +6,6 @@ import { RequestHandlerContext } from 'src/core/server'; import { schema } from '@kbn/config-schema'; -import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; import { calendarSchema } from './schemas/calendars_schema'; @@ -42,13 +41,13 @@ function getCalendarsByIds(context: RequestHandlerContext, calendarIds: string) return cal.getCalendarsByIds(calendarIds); } -export function calendars({ router, getLicenseCheckResults }: RouteInitialization) { +export function calendars({ router, mlLicense }: RouteInitialization) { router.get( { path: '/api/ml/calendars', validate: false, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const resp = await getAllCalendars(context); @@ -68,7 +67,7 @@ export function calendars({ router, getLicenseCheckResults }: RouteInitializatio params: schema.object({ calendarIds: schema.string() }), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { let returnValue; try { const calendarIds = request.params.calendarIds.split(','); @@ -95,7 +94,7 @@ export function calendars({ router, getLicenseCheckResults }: RouteInitializatio body: schema.object({ ...calendarSchema }), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const body = request.body; const resp = await newCalendar(context, body); @@ -117,7 +116,7 @@ export function calendars({ router, getLicenseCheckResults }: RouteInitializatio body: schema.object({ ...calendarSchema }), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { calendarId } = request.params; const body = request.body; @@ -139,7 +138,7 @@ export function calendars({ router, getLicenseCheckResults }: RouteInitializatio params: schema.object({ calendarId: schema.string() }), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { calendarId } = request.params; const resp = await deleteCalendar(context, calendarId); diff --git a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts index 0a93320c05eb5..7ed1aa02b24ab 100644 --- a/x-pack/plugins/ml/server/routes/data_frame_analytics.ts +++ b/x-pack/plugins/ml/server/routes/data_frame_analytics.ts @@ -7,7 +7,6 @@ import { schema } from '@kbn/config-schema'; import { wrapError } from '../client/error_wrapper'; import { analyticsAuditMessagesProvider } from '../models/data_frame_analytics/analytics_audit_messages'; -import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; import { RouteInitialization } from '../types'; import { dataAnalyticsJobConfigSchema, @@ -18,7 +17,7 @@ import { /** * Routes for the data frame analytics */ -export function dataFrameAnalyticsRoutes({ router, getLicenseCheckResults }: RouteInitialization) { +export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitialization) { /** * @apiGroup DataFrameAnalytics * @@ -36,7 +35,7 @@ export function dataFrameAnalyticsRoutes({ router, getLicenseCheckResults }: Rou params: schema.object({ analyticsId: schema.maybe(schema.string()) }), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const results = await context.ml!.mlClient.callAsCurrentUser('ml.getDataFrameAnalytics'); return response.ok({ @@ -64,7 +63,7 @@ export function dataFrameAnalyticsRoutes({ router, getLicenseCheckResults }: Rou params: schema.object({ analyticsId: schema.string() }), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { analyticsId } = request.params; const results = await context.ml!.mlClient.callAsCurrentUser('ml.getDataFrameAnalytics', { @@ -91,7 +90,7 @@ export function dataFrameAnalyticsRoutes({ router, getLicenseCheckResults }: Rou path: '/api/ml/data_frame/analytics/_stats', validate: false, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const results = await context.ml!.mlClient.callAsCurrentUser( 'ml.getDataFrameAnalyticsStats' @@ -121,7 +120,7 @@ export function dataFrameAnalyticsRoutes({ router, getLicenseCheckResults }: Rou params: schema.object({ analyticsId: schema.string() }), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { analyticsId } = request.params; const results = await context.ml!.mlClient.callAsCurrentUser( @@ -159,7 +158,7 @@ export function dataFrameAnalyticsRoutes({ router, getLicenseCheckResults }: Rou body: schema.object(dataAnalyticsJobConfigSchema), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { analyticsId } = request.params; const results = await context.ml!.mlClient.callAsCurrentUser( @@ -192,7 +191,7 @@ export function dataFrameAnalyticsRoutes({ router, getLicenseCheckResults }: Rou body: schema.object({ ...dataAnalyticsEvaluateSchema }), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const results = await context.ml!.mlClient.callAsCurrentUser( 'ml.evaluateDataFrameAnalytics', @@ -232,7 +231,7 @@ export function dataFrameAnalyticsRoutes({ router, getLicenseCheckResults }: Rou body: schema.object({ ...dataAnalyticsExplainSchema }), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const results = await context.ml!.mlClient.callAsCurrentUser( 'ml.explainDataFrameAnalytics', @@ -267,7 +266,7 @@ export function dataFrameAnalyticsRoutes({ router, getLicenseCheckResults }: Rou }), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { analyticsId } = request.params; const results = await context.ml!.mlClient.callAsCurrentUser( @@ -303,7 +302,7 @@ export function dataFrameAnalyticsRoutes({ router, getLicenseCheckResults }: Rou }), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { analyticsId } = request.params; const results = await context.ml!.mlClient.callAsCurrentUser('ml.startDataFrameAnalytics', { @@ -337,7 +336,7 @@ export function dataFrameAnalyticsRoutes({ router, getLicenseCheckResults }: Rou }), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const options: { analyticsId: string; force?: boolean | undefined } = { analyticsId: request.params.analyticsId, @@ -377,7 +376,7 @@ export function dataFrameAnalyticsRoutes({ router, getLicenseCheckResults }: Rou params: schema.object({ analyticsId: schema.string() }), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { analyticsId } = request.params; const { getAnalyticsAuditMessages } = analyticsAuditMessagesProvider( diff --git a/x-pack/plugins/ml/server/routes/data_visualizer.ts b/x-pack/plugins/ml/server/routes/data_visualizer.ts index e4d068784def1..b37c80b815e1a 100644 --- a/x-pack/plugins/ml/server/routes/data_visualizer.ts +++ b/x-pack/plugins/ml/server/routes/data_visualizer.ts @@ -12,7 +12,6 @@ import { dataVisualizerFieldStatsSchema, dataVisualizerOverallStatsSchema, } from './schemas/data_visualizer_schema'; -import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; import { RouteInitialization } from '../types'; function getOverallStats( @@ -68,7 +67,7 @@ function getStatsForFields( /** * Routes for the index data visualizer. */ -export function dataVisualizerRoutes({ router, getLicenseCheckResults }: RouteInitialization) { +export function dataVisualizerRoutes({ router, mlLicense }: RouteInitialization) { /** * @apiGroup DataVisualizer * @@ -83,7 +82,7 @@ export function dataVisualizerRoutes({ router, getLicenseCheckResults }: RouteIn path: '/api/ml/data_visualizer/get_field_stats/{indexPatternTitle}', validate: dataVisualizerFieldStatsSchema, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { const { params: { indexPatternTitle }, @@ -135,7 +134,7 @@ export function dataVisualizerRoutes({ router, getLicenseCheckResults }: RouteIn path: '/api/ml/data_visualizer/get_overall_stats/{indexPatternTitle}', validate: dataVisualizerOverallStatsSchema, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { const { params: { indexPatternTitle }, diff --git a/x-pack/plugins/ml/server/routes/datafeeds.ts b/x-pack/plugins/ml/server/routes/datafeeds.ts index e3bce4c1328e4..c1ee839340996 100644 --- a/x-pack/plugins/ml/server/routes/datafeeds.ts +++ b/x-pack/plugins/ml/server/routes/datafeeds.ts @@ -5,7 +5,6 @@ */ import { schema } from '@kbn/config-schema'; -import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; import { startDatafeedSchema, datafeedConfigSchema } from './schemas/datafeeds_schema'; @@ -13,7 +12,7 @@ import { startDatafeedSchema, datafeedConfigSchema } from './schemas/datafeeds_s /** * Routes for datafeed service */ -export function dataFeedRoutes({ router, getLicenseCheckResults }: RouteInitialization) { +export function dataFeedRoutes({ router, mlLicense }: RouteInitialization) { /** * @apiGroup DatafeedService * @@ -26,7 +25,7 @@ export function dataFeedRoutes({ router, getLicenseCheckResults }: RouteInitiali path: '/api/ml/datafeeds', validate: false, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const resp = await context.ml!.mlClient.callAsCurrentUser('ml.datafeeds'); @@ -53,7 +52,7 @@ export function dataFeedRoutes({ router, getLicenseCheckResults }: RouteInitiali params: schema.object({ datafeedId: schema.string() }), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const datafeedId = request.params.datafeedId; const resp = await context.ml!.mlClient.callAsCurrentUser('ml.datafeeds', { datafeedId }); @@ -79,7 +78,7 @@ export function dataFeedRoutes({ router, getLicenseCheckResults }: RouteInitiali path: '/api/ml/datafeeds/_stats', validate: false, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const resp = await context.ml!.mlClient.callAsCurrentUser('ml.datafeedStats'); @@ -106,7 +105,7 @@ export function dataFeedRoutes({ router, getLicenseCheckResults }: RouteInitiali params: schema.object({ datafeedId: schema.string() }), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const datafeedId = request.params.datafeedId; const resp = await context.ml!.mlClient.callAsCurrentUser('ml.datafeedStats', { @@ -137,7 +136,7 @@ export function dataFeedRoutes({ router, getLicenseCheckResults }: RouteInitiali body: datafeedConfigSchema, }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const datafeedId = request.params.datafeedId; const resp = await context.ml!.mlClient.callAsCurrentUser('ml.addDatafeed', { @@ -169,7 +168,7 @@ export function dataFeedRoutes({ router, getLicenseCheckResults }: RouteInitiali body: datafeedConfigSchema, }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const datafeedId = request.params.datafeedId; const resp = await context.ml!.mlClient.callAsCurrentUser('ml.updateDatafeed', { @@ -201,7 +200,7 @@ export function dataFeedRoutes({ router, getLicenseCheckResults }: RouteInitiali query: schema.maybe(schema.object({ force: schema.maybe(schema.any()) })), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const options: { datafeedId: string; force?: boolean } = { datafeedId: request.params.jobId, @@ -237,7 +236,7 @@ export function dataFeedRoutes({ router, getLicenseCheckResults }: RouteInitiali body: startDatafeedSchema, }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const datafeedId = request.params.datafeedId; const { start, end } = request.body; @@ -271,7 +270,7 @@ export function dataFeedRoutes({ router, getLicenseCheckResults }: RouteInitiali params: schema.object({ datafeedId: schema.string() }), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const datafeedId = request.params.datafeedId; @@ -302,7 +301,7 @@ export function dataFeedRoutes({ router, getLicenseCheckResults }: RouteInitiali params: schema.object({ datafeedId: schema.string() }), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const datafeedId = request.params.datafeedId; const resp = await context.ml!.mlClient.callAsCurrentUser('ml.datafeedPreview', { diff --git a/x-pack/plugins/ml/server/routes/fields_service.ts b/x-pack/plugins/ml/server/routes/fields_service.ts index bc092190c2c62..f4d4e5759a105 100644 --- a/x-pack/plugins/ml/server/routes/fields_service.ts +++ b/x-pack/plugins/ml/server/routes/fields_service.ts @@ -5,7 +5,6 @@ */ import { RequestHandlerContext } from 'src/core/server'; -import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; import { @@ -29,7 +28,7 @@ function getTimeFieldRange(context: RequestHandlerContext, payload: any) { /** * Routes for fields service */ -export function fieldsService({ router, getLicenseCheckResults }: RouteInitialization) { +export function fieldsService({ router, mlLicense }: RouteInitialization) { /** * @apiGroup FieldsService * @@ -44,7 +43,8 @@ export function fieldsService({ router, getLicenseCheckResults }: RouteInitializ body: getCardinalityOfFieldsSchema, }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const resp = await getCardinalityOfFields(context, request.body); @@ -71,7 +71,7 @@ export function fieldsService({ router, getLicenseCheckResults }: RouteInitializ body: getTimeFieldRangeSchema, }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { const resp = await getTimeFieldRange(context, request.body); diff --git a/x-pack/plugins/ml/server/routes/file_data_visualizer.ts b/x-pack/plugins/ml/server/routes/file_data_visualizer.ts index 1d724a8843350..69ec79704deee 100644 --- a/x-pack/plugins/ml/server/routes/file_data_visualizer.ts +++ b/x-pack/plugins/ml/server/routes/file_data_visualizer.ts @@ -18,7 +18,6 @@ import { Mappings, } from '../models/file_data_visualizer'; -import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; import { RouteInitialization } from '../types'; import { incrementFileDataVisualizerIndexCreationCount } from '../lib/ml_telemetry'; @@ -43,7 +42,7 @@ function importData( /** * Routes for the file data visualizer. */ -export function fileDataVisualizerRoutes({ router, getLicenseCheckResults }: RouteInitialization) { +export function fileDataVisualizerRoutes({ router, mlLicense }: RouteInitialization) { /** * @apiGroup FileDataVisualizer * @@ -82,7 +81,7 @@ export function fileDataVisualizerRoutes({ router, getLicenseCheckResults }: Rou }, }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { const result = await analyzeFiles(context, request.body, request.query); return response.ok({ body: result }); @@ -124,7 +123,7 @@ export function fileDataVisualizerRoutes({ router, getLicenseCheckResults }: Rou }, }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { const { id } = request.query; const { index, data, settings, mappings, ingestPipeline } = request.body; diff --git a/x-pack/plugins/ml/server/routes/filters.ts b/x-pack/plugins/ml/server/routes/filters.ts index d5530668b2606..1f8891c247c67 100644 --- a/x-pack/plugins/ml/server/routes/filters.ts +++ b/x-pack/plugins/ml/server/routes/filters.ts @@ -6,7 +6,6 @@ import { RequestHandlerContext } from 'src/core/server'; import { schema } from '@kbn/config-schema'; -import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; import { createFilterSchema, updateFilterSchema } from './schemas/filters_schema'; @@ -44,7 +43,7 @@ function deleteFilter(context: RequestHandlerContext, filterId: string) { return mgr.deleteFilter(filterId); } -export function filtersRoutes({ router, getLicenseCheckResults }: RouteInitialization) { +export function filtersRoutes({ router, mlLicense }: RouteInitialization) { /** * @apiGroup Filters * @@ -60,7 +59,7 @@ export function filtersRoutes({ router, getLicenseCheckResults }: RouteInitializ path: '/api/ml/filters', validate: false, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const resp = await getAllFilters(context); @@ -90,7 +89,7 @@ export function filtersRoutes({ router, getLicenseCheckResults }: RouteInitializ params: schema.object({ filterId: schema.string() }), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const resp = await getFilter(context, request.params.filterId); return response.ok({ @@ -119,7 +118,7 @@ export function filtersRoutes({ router, getLicenseCheckResults }: RouteInitializ body: schema.object(createFilterSchema), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const body = request.body; const resp = await newFilter(context, body); @@ -151,7 +150,7 @@ export function filtersRoutes({ router, getLicenseCheckResults }: RouteInitializ body: schema.object(updateFilterSchema), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { filterId } = request.params; const body = request.body; @@ -182,7 +181,7 @@ export function filtersRoutes({ router, getLicenseCheckResults }: RouteInitializ params: schema.object({ filterId: schema.string() }), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { filterId } = request.params; const resp = await deleteFilter(context, filterId); @@ -212,7 +211,7 @@ export function filtersRoutes({ router, getLicenseCheckResults }: RouteInitializ path: '/api/ml/filters/_stats', validate: false, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const resp = await getAllFilterStats(context); diff --git a/x-pack/plugins/ml/server/routes/indices.ts b/x-pack/plugins/ml/server/routes/indices.ts index e01a7a0cbad28..fe66cc8b01396 100644 --- a/x-pack/plugins/ml/server/routes/indices.ts +++ b/x-pack/plugins/ml/server/routes/indices.ts @@ -6,13 +6,12 @@ import { schema } from '@kbn/config-schema'; import { wrapError } from '../client/error_wrapper'; -import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; import { RouteInitialization } from '../types'; /** * Indices routes. */ -export function indicesRoutes({ router, getLicenseCheckResults }: RouteInitialization) { +export function indicesRoutes({ router, mlLicense }: RouteInitialization) { /** * @apiGroup Indices * @@ -30,7 +29,7 @@ export function indicesRoutes({ router, getLicenseCheckResults }: RouteInitializ }), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { body: { index, fields: requestFields }, diff --git a/x-pack/plugins/ml/server/routes/job_audit_messages.ts b/x-pack/plugins/ml/server/routes/job_audit_messages.ts index 38df28e17ec0d..5c6d8023cc172 100644 --- a/x-pack/plugins/ml/server/routes/job_audit_messages.ts +++ b/x-pack/plugins/ml/server/routes/job_audit_messages.ts @@ -5,7 +5,6 @@ */ import { schema } from '@kbn/config-schema'; -import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; import { jobAuditMessagesProvider } from '../models/job_audit_messages'; @@ -13,7 +12,7 @@ import { jobAuditMessagesProvider } from '../models/job_audit_messages'; /** * Routes for job audit message routes */ -export function jobAuditMessagesRoutes({ router, getLicenseCheckResults }: RouteInitialization) { +export function jobAuditMessagesRoutes({ router, mlLicense }: RouteInitialization) { /** * @apiGroup JobAuditMessages * @@ -29,7 +28,7 @@ export function jobAuditMessagesRoutes({ router, getLicenseCheckResults }: Route query: schema.maybe(schema.object({ from: schema.maybe(schema.any()) })), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { getJobAuditMessages } = jobAuditMessagesProvider( context.ml!.mlClient.callAsCurrentUser @@ -62,7 +61,7 @@ export function jobAuditMessagesRoutes({ router, getLicenseCheckResults }: Route query: schema.maybe(schema.object({ from: schema.maybe(schema.any()) })), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { getJobAuditMessages } = jobAuditMessagesProvider( context.ml!.mlClient.callAsCurrentUser diff --git a/x-pack/plugins/ml/server/routes/job_service.ts b/x-pack/plugins/ml/server/routes/job_service.ts index e15888088d3a1..9ad2f80a1e66b 100644 --- a/x-pack/plugins/ml/server/routes/job_service.ts +++ b/x-pack/plugins/ml/server/routes/job_service.ts @@ -7,7 +7,6 @@ import Boom from 'boom'; import { schema } from '@kbn/config-schema'; import { IScopedClusterClient } from 'src/core/server'; -import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; import { @@ -28,12 +27,11 @@ import { categorizationExamplesProvider } from '../models/job_service/new_job'; /** * Routes for job service */ -export function jobServiceRoutes({ router, getLicenseCheckResults }: RouteInitialization) { +export function jobServiceRoutes({ router, mlLicense }: RouteInitialization) { async function hasPermissionToCreateJobs( callAsCurrentUser: IScopedClusterClient['callAsCurrentUser'] ) { - const { isSecurityDisabled } = getLicenseCheckResults(); - if (isSecurityDisabled === true) { + if (mlLicense.isSecurityEnabled() === false) { return true; } @@ -63,7 +61,7 @@ export function jobServiceRoutes({ router, getLicenseCheckResults }: RouteInitia body: schema.object(forceStartDatafeedSchema), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { forceStartDatafeeds } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const { datafeedIds, start, end } = request.body; @@ -92,7 +90,7 @@ export function jobServiceRoutes({ router, getLicenseCheckResults }: RouteInitia body: schema.object(datafeedIdsSchema), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { stopDatafeeds } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const { datafeedIds } = request.body; @@ -121,7 +119,7 @@ export function jobServiceRoutes({ router, getLicenseCheckResults }: RouteInitia body: schema.object(jobIdsSchema), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { deleteJobs } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const { jobIds } = request.body; @@ -150,7 +148,7 @@ export function jobServiceRoutes({ router, getLicenseCheckResults }: RouteInitia body: schema.object(jobIdsSchema), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { closeJobs } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const { jobIds } = request.body; @@ -179,7 +177,7 @@ export function jobServiceRoutes({ router, getLicenseCheckResults }: RouteInitia body: schema.object(jobIdsSchema), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { jobsSummary } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const { jobIds } = request.body; @@ -208,7 +206,7 @@ export function jobServiceRoutes({ router, getLicenseCheckResults }: RouteInitia body: schema.object(jobsWithTimerangeSchema), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { jobsWithTimerange } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const { dateFormatTz } = request.body; @@ -237,7 +235,7 @@ export function jobServiceRoutes({ router, getLicenseCheckResults }: RouteInitia body: schema.object(jobIdsSchema), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { createFullJobsList } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const { jobIds } = request.body; @@ -264,7 +262,7 @@ export function jobServiceRoutes({ router, getLicenseCheckResults }: RouteInitia path: '/api/ml/jobs/groups', validate: false, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { getAllGroups } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const resp = await getAllGroups(); @@ -292,7 +290,7 @@ export function jobServiceRoutes({ router, getLicenseCheckResults }: RouteInitia body: schema.object(updateGroupsSchema), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { updateGroups } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const { jobs } = request.body; @@ -319,7 +317,7 @@ export function jobServiceRoutes({ router, getLicenseCheckResults }: RouteInitia path: '/api/ml/jobs/deleting_jobs_tasks', validate: false, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { deletingJobTasks } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const resp = await deletingJobTasks(); @@ -347,7 +345,7 @@ export function jobServiceRoutes({ router, getLicenseCheckResults }: RouteInitia body: schema.object(jobIdsSchema), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { jobsExist } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const { jobIds } = request.body; @@ -377,7 +375,7 @@ export function jobServiceRoutes({ router, getLicenseCheckResults }: RouteInitia query: schema.maybe(schema.object({ rollup: schema.maybe(schema.string()) })), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { indexPattern } = request.params; const isRollup = request.query.rollup === 'true'; @@ -408,7 +406,7 @@ export function jobServiceRoutes({ router, getLicenseCheckResults }: RouteInitia body: schema.object(chartSchema), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { indexPatternTitle, @@ -461,7 +459,7 @@ export function jobServiceRoutes({ router, getLicenseCheckResults }: RouteInitia body: schema.object(chartSchema), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { indexPatternTitle, @@ -509,7 +507,7 @@ export function jobServiceRoutes({ router, getLicenseCheckResults }: RouteInitia path: '/api/ml/jobs/all_jobs_and_group_ids', validate: false, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { getAllJobAndGroupIds } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const resp = await getAllJobAndGroupIds(); @@ -537,7 +535,7 @@ export function jobServiceRoutes({ router, getLicenseCheckResults }: RouteInitia body: schema.object(lookBackProgressSchema), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { getLookBackProgress } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const { jobId, start, end } = request.body; @@ -566,7 +564,7 @@ export function jobServiceRoutes({ router, getLicenseCheckResults }: RouteInitia body: schema.object(categorizationFieldExamplesSchema), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { // due to the use of the _analyze endpoint which is called by the kibana user, // basic job creation privileges are required to use this endpoint @@ -625,7 +623,7 @@ export function jobServiceRoutes({ router, getLicenseCheckResults }: RouteInitia body: schema.object(topCategoriesSchema), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { topCategories } = jobServiceProvider(context.ml!.mlClient.callAsCurrentUser); const { jobId, count } = request.body; diff --git a/x-pack/plugins/ml/server/routes/job_validation.ts b/x-pack/plugins/ml/server/routes/job_validation.ts index ae2e6885ba0f3..7d5a7a2285977 100644 --- a/x-pack/plugins/ml/server/routes/job_validation.ts +++ b/x-pack/plugins/ml/server/routes/job_validation.ts @@ -7,7 +7,6 @@ import Boom from 'boom'; import { RequestHandlerContext } from 'src/core/server'; import { schema, TypeOf } from '@kbn/config-schema'; -import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; import { @@ -25,10 +24,7 @@ type CalculateModelMemoryLimitPayload = TypeOf; /** * Routes for job validation */ -export function jobValidationRoutes( - { getLicenseCheckResults, router }: RouteInitialization, - version: string -) { +export function jobValidationRoutes({ router, mlLicense }: RouteInitialization, version: string) { function calculateModelMemoryLimit( context: RequestHandlerContext, payload: CalculateModelMemoryLimitPayload @@ -70,13 +66,13 @@ export function jobValidationRoutes( body: estimateBucketSpanSchema, }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { let errorResp; const resp = await estimateBucketSpanFactory( context.ml!.mlClient.callAsCurrentUser, context.core.elasticsearch.adminClient.callAsInternalUser, - getLicenseCheckResults().isSecurityDisabled + mlLicense.isSecurityEnabled() === false )(request.body) // this catch gets triggered when the estimation code runs without error // but isn't able to come up with a bucket span estimation. @@ -117,7 +113,7 @@ export function jobValidationRoutes( body: modelMemoryLimitSchema, }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const resp = await calculateModelMemoryLimit(context, request.body); @@ -144,7 +140,7 @@ export function jobValidationRoutes( body: schema.object(validateCardinalitySchema), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const resp = await validateCardinality( context.ml!.mlClient.callAsCurrentUser, @@ -174,7 +170,7 @@ export function jobValidationRoutes( body: validateJobSchema, }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { // version corresponds to the version used in documentation links. const resp = await validateJob( @@ -182,7 +178,7 @@ export function jobValidationRoutes( request.body, version, context.core.elasticsearch.adminClient.callAsInternalUser, - getLicenseCheckResults().isSecurityDisabled + mlLicense.isSecurityEnabled() === false ); return response.ok({ diff --git a/x-pack/plugins/ml/server/routes/license_check_pre_routing_factory.ts b/x-pack/plugins/ml/server/routes/license_check_pre_routing_factory.ts deleted file mode 100644 index a371af1abf2d1..0000000000000 --- a/x-pack/plugins/ml/server/routes/license_check_pre_routing_factory.ts +++ /dev/null @@ -1,33 +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 { - KibanaRequest, - KibanaResponseFactory, - RequestHandler, - RequestHandlerContext, -} from 'src/core/server'; -import { LicenseCheckResult } from '../types'; - -export const licensePreRoutingFactory = ( - getLicenseCheckResults: () => LicenseCheckResult, - handler: RequestHandler -): RequestHandler => { - // License checking and enable/disable logic - return function licensePreRouting( - ctx: RequestHandlerContext, - request: KibanaRequest, - response: KibanaResponseFactory - ) { - const licenseCheckResults = getLicenseCheckResults(); - - if (!licenseCheckResults.isAvailable) { - return response.forbidden(); - } - - return handler(ctx, request, response); - }; -}; diff --git a/x-pack/plugins/ml/server/routes/modules.ts b/x-pack/plugins/ml/server/routes/modules.ts index c9b005d4e43f9..a51718acb7425 100644 --- a/x-pack/plugins/ml/server/routes/modules.ts +++ b/x-pack/plugins/ml/server/routes/modules.ts @@ -9,7 +9,6 @@ import { RequestHandlerContext } from 'kibana/server'; import { DatafeedOverride, JobOverride } from '../../../../legacy/plugins/ml/common/types/modules'; import { wrapError } from '../client/error_wrapper'; import { DataRecognizer } from '../models/data_recognizer'; -import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; import { getModuleIdParamSchema, setupModuleBodySchema } from './schemas/modules'; import { RouteInitialization } from '../types'; @@ -65,7 +64,7 @@ function dataRecognizerJobsExist(context: RequestHandlerContext, moduleId: strin /** * Recognizer routes. */ -export function dataRecognizer({ router, getLicenseCheckResults }: RouteInitialization) { +export function dataRecognizer({ router, mlLicense }: RouteInitialization) { /** * @apiGroup DataRecognizer * @@ -84,7 +83,7 @@ export function dataRecognizer({ router, getLicenseCheckResults }: RouteInitiali }), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { indexPatternTitle } = request.params; const results = await recognize(context, indexPatternTitle); @@ -114,7 +113,7 @@ export function dataRecognizer({ router, getLicenseCheckResults }: RouteInitiali }), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { let { moduleId } = request.params; if (moduleId === '') { @@ -150,7 +149,7 @@ export function dataRecognizer({ router, getLicenseCheckResults }: RouteInitiali body: setupModuleBodySchema, }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { moduleId } = request.params; @@ -207,7 +206,7 @@ export function dataRecognizer({ router, getLicenseCheckResults }: RouteInitiali }), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const { moduleId } = request.params; const result = await dataRecognizerJobsExist(context, moduleId); diff --git a/x-pack/plugins/ml/server/routes/notification_settings.ts b/x-pack/plugins/ml/server/routes/notification_settings.ts index b68d2441333f9..59458b1e486db 100644 --- a/x-pack/plugins/ml/server/routes/notification_settings.ts +++ b/x-pack/plugins/ml/server/routes/notification_settings.ts @@ -4,14 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; /** * Routes for notification settings */ -export function notificationRoutes({ router, getLicenseCheckResults }: RouteInitialization) { +export function notificationRoutes({ router, mlLicense }: RouteInitialization) { /** * @apiGroup NotificationSettings * @@ -24,7 +23,7 @@ export function notificationRoutes({ router, getLicenseCheckResults }: RouteInit path: '/api/ml/notification_settings', validate: false, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const params = { includeDefaults: true, diff --git a/x-pack/plugins/ml/server/routes/results_service.ts b/x-pack/plugins/ml/server/routes/results_service.ts index 77c998acc9f27..7a12e5196b9a5 100644 --- a/x-pack/plugins/ml/server/routes/results_service.ts +++ b/x-pack/plugins/ml/server/routes/results_service.ts @@ -6,7 +6,6 @@ import { RequestHandlerContext } from 'src/core/server'; import { schema } from '@kbn/config-schema'; -import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; import { wrapError } from '../client/error_wrapper'; import { RouteInitialization } from '../types'; import { @@ -74,7 +73,7 @@ function getPartitionFieldsValues(context: RequestHandlerContext, payload: any) /** * Routes for results service */ -export function resultsServiceRoutes({ router, getLicenseCheckResults }: RouteInitialization) { +export function resultsServiceRoutes({ router, mlLicense }: RouteInitialization) { /** * @apiGroup ResultsService * @@ -89,7 +88,7 @@ export function resultsServiceRoutes({ router, getLicenseCheckResults }: RouteIn body: schema.object(anomaliesTableDataSchema), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const resp = await getAnomaliesTableData(context, request.body); @@ -116,7 +115,7 @@ export function resultsServiceRoutes({ router, getLicenseCheckResults }: RouteIn body: schema.object(categoryDefinitionSchema), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const resp = await getCategoryDefinition(context, request.body); @@ -143,7 +142,7 @@ export function resultsServiceRoutes({ router, getLicenseCheckResults }: RouteIn body: schema.object(maxAnomalyScoreSchema), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const resp = await getMaxAnomalyScore(context, request.body); @@ -170,7 +169,7 @@ export function resultsServiceRoutes({ router, getLicenseCheckResults }: RouteIn body: schema.object(categoryExamplesSchema), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const resp = await getCategoryExamples(context, request.body); @@ -197,7 +196,7 @@ export function resultsServiceRoutes({ router, getLicenseCheckResults }: RouteIn body: schema.object(partitionFieldValuesSchema), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { const resp = await getPartitionFieldsValues(context, request.body); diff --git a/x-pack/plugins/ml/server/routes/system.ts b/x-pack/plugins/ml/server/routes/system.ts index 36a9ea1447f58..a0d7d312c04d4 100644 --- a/x-pack/plugins/ml/server/routes/system.ts +++ b/x-pack/plugins/ml/server/routes/system.ts @@ -12,14 +12,13 @@ import { wrapError } from '../client/error_wrapper'; import { mlLog } from '../client/log'; import { privilegesProvider } from '../lib/check_privileges'; import { spacesUtilsProvider } from '../lib/spaces_utils'; -import { licensePreRoutingFactory } from './license_check_pre_routing_factory'; import { RouteInitialization, SystemRouteDeps } from '../types'; /** * System routes */ export function systemRoutes( - { getLicenseCheckResults, router }: RouteInitialization, + { router, mlLicense }: RouteInitialization, { spacesPlugin, cloud }: SystemRouteDeps ) { async function getNodeCount(context: RequestHandlerContext) { @@ -56,7 +55,7 @@ export function systemRoutes( body: schema.maybe(schema.any()), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { let upgradeInProgress = false; try { @@ -77,7 +76,7 @@ export function systemRoutes( } } - if (getLicenseCheckResults().isSecurityDisabled) { + if (mlLicense.isSecurityEnabled() === false) { // if xpack.security.enabled has been explicitly set to false // return that security is disabled and don't call the privilegeCheck endpoint return response.ok({ @@ -116,7 +115,7 @@ export function systemRoutes( }), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { const ignoreSpaces = request.query && request.query.ignoreSpaces === 'true'; // if spaces is disabled force isMlEnabledInSpace to be true @@ -127,7 +126,7 @@ export function systemRoutes( const { getPrivileges } = privilegesProvider( context.ml!.mlClient.callAsCurrentUser, - getLicenseCheckResults(), + mlLicense, isMlEnabledInSpace, ignoreSpaces ); @@ -152,11 +151,11 @@ export function systemRoutes( path: '/api/ml/ml_node_count', validate: false, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { // check for basic license first for consistency with other // security disabled checks - if (getLicenseCheckResults().isSecurityDisabled) { + if (mlLicense.isSecurityEnabled() === false) { return response.ok({ body: await getNodeCount(context), }); @@ -203,7 +202,7 @@ export function systemRoutes( path: '/api/ml/info', validate: false, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.basicLicenseAPIGuard(async (context, request, response) => { try { const info = await context.ml!.mlClient.callAsCurrentUser('ml.info'); const cloudId = cloud && cloud.cloudId; @@ -231,7 +230,7 @@ export function systemRoutes( body: schema.maybe(schema.any()), }, }, - licensePreRoutingFactory(getLicenseCheckResults, async (context, request, response) => { + mlLicense.fullLicenseAPIGuard(async (context, request, response) => { try { return response.ok({ body: await context.ml!.mlClient.callAsCurrentUser('search', request.body), diff --git a/x-pack/plugins/ml/server/types.ts b/x-pack/plugins/ml/server/types.ts index 550abadb3c06f..aeb4c505ec55e 100644 --- a/x-pack/plugins/ml/server/types.ts +++ b/x-pack/plugins/ml/server/types.ts @@ -12,6 +12,7 @@ import { SecurityPluginSetup } from '../../security/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { SpacesPluginSetup } from '../../spaces/server'; +import { MlServerLicense } from './lib/license'; export interface LicenseCheckResult { isAvailable: boolean; @@ -39,5 +40,5 @@ export interface PluginsSetup { export interface RouteInitialization { router: IRouter; - getLicenseCheckResults: () => LicenseCheckResult; + mlLicense: MlServerLicense; } diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_edit.test.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_edit.test.js index 1136b7307176d..f7625d9eec090 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_edit.test.js +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_edit.test.js @@ -6,22 +6,28 @@ jest.mock('ui/new_platform'); import { RemoteClusterForm } from '../../public/application/sections/components/remote_cluster_form'; -import { pageHelpers, setupEnvironment, nextTick } from './helpers'; +// import { pageHelpers, setupEnvironment, nextTick } from './helpers'; +import { pageHelpers, nextTick } from './helpers'; import { REMOTE_CLUSTER_EDIT, REMOTE_CLUSTER_EDIT_NAME } from './helpers/constants'; -const { setup } = pageHelpers.remoteClustersEdit; +// const { setup } = pageHelpers.remoteClustersEdit; const { setup: setupRemoteClustersAdd } = pageHelpers.remoteClustersAdd; // FLAKY: https://github.com/elastic/kibana/issues/57762 // FLAKY: https://github.com/elastic/kibana/issues/57997 // FLAKY: https://github.com/elastic/kibana/issues/57998 describe.skip('Edit Remote cluster', () => { - let server; - let httpRequestsMockHelpers; + // let server; + // let httpRequestsMockHelpers; let component; let find; let exists; + /** + * + * commented out due to hooks being called regardless of skip + * https://github.com/facebook/jest/issues/8379 + beforeAll(() => { ({ server, httpRequestsMockHelpers } = setupEnvironment()); }); @@ -38,6 +44,8 @@ describe.skip('Edit Remote cluster', () => { component.update(); }); + */ + test('should have the title of the page set correctly', () => { expect(exists('remoteClusterPageTitle')).toBe(true); expect(find('remoteClusterPageTitle').text()).toEqual('Edit remote cluster'); diff --git a/x-pack/plugins/security/common/model/index.ts b/x-pack/plugins/security/common/model/index.ts index 121791d113bd5..88da416cf715b 100644 --- a/x-pack/plugins/security/common/model/index.ts +++ b/x-pack/plugins/security/common/model/index.ts @@ -15,10 +15,12 @@ export { RoleIndexPrivilege, RoleKibanaPrivilege, copyRole, - isReadOnlyRole, - isReservedRole, + isRoleDeprecated, + isRoleReadOnly, + isRoleReserved, isRoleEnabled, prepareRoleClone, + getExtendedRoleDeprecationNotice, } from './role'; export { KibanaPrivileges } from './kibana_privileges'; export { diff --git a/x-pack/plugins/security/common/model/role.test.ts b/x-pack/plugins/security/common/model/role.test.ts index d4a910a1785eb..b17e264f3cdd8 100644 --- a/x-pack/plugins/security/common/model/role.test.ts +++ b/x-pack/plugins/security/common/model/role.test.ts @@ -4,7 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Role, isReadOnlyRole, isReservedRole, isRoleEnabled, copyRole, prepareRoleClone } from '.'; +import { + Role, + isRoleEnabled, + isRoleReserved, + isRoleDeprecated, + isRoleReadOnly, + copyRole, + prepareRoleClone, + getExtendedRoleDeprecationNotice, +} from '../../common/model'; describe('role', () => { describe('isRoleEnabled', () => { @@ -32,14 +41,14 @@ describe('role', () => { }); }); - describe('isReservedRole', () => { + describe('isRoleReserved', () => { test('should return false if role is explicitly not reserved', () => { const testRole = { metadata: { _reserved: false, }, }; - expect(isReservedRole(testRole)).toBe(false); + expect(isRoleReserved(testRole)).toBe(false); }); test('should return true if role is explicitly reserved', () => { @@ -48,30 +57,74 @@ describe('role', () => { _reserved: true, }, }; - expect(isReservedRole(testRole)).toBe(true); + expect(isRoleReserved(testRole)).toBe(true); }); test('should return false if role is NOT explicitly reserved or not reserved', () => { const testRole = {}; - expect(isReservedRole(testRole)).toBe(false); + expect(isRoleReserved(testRole)).toBe(false); }); }); - describe('isReadOnlyRole', () => { + describe('isRoleDeprecated', () => { + test('should return false if role is explicitly not deprecated', () => { + const testRole = { + metadata: { + _deprecated: false, + }, + }; + expect(isRoleDeprecated(testRole)).toBe(false); + }); + + test('should return true if role is explicitly deprecated', () => { + const testRole = { + metadata: { + _deprecated: true, + }, + }; + expect(isRoleDeprecated(testRole)).toBe(true); + }); + + test('should return false if role is NOT explicitly deprecated or not deprecated', () => { + const testRole = {}; + expect(isRoleDeprecated(testRole)).toBe(false); + }); + }); + + describe('getExtendedRoleDeprecationNotice', () => { + test('advises not to use the deprecated role', () => { + const testRole = { name: 'test-role' }; + expect(getExtendedRoleDeprecationNotice(testRole)).toMatchInlineSnapshot( + `"The test-role role is deprecated. "` + ); + }); + + test('includes the deprecation reason when provided', () => { + const testRole = { + name: 'test-role', + metadata: { _deprecated_reason: "We just don't like this role anymore" }, + }; + expect(getExtendedRoleDeprecationNotice(testRole)).toMatchInlineSnapshot( + `"The test-role role is deprecated. We just don't like this role anymore"` + ); + }); + }); + + describe('isRoleReadOnly', () => { test('returns true for reserved roles', () => { const testRole = { metadata: { _reserved: true, }, }; - expect(isReadOnlyRole(testRole)).toBe(true); + expect(isRoleReadOnly(testRole)).toBe(true); }); test('returns true for roles with transform errors', () => { const testRole = { _transform_error: ['kibana'], }; - expect(isReadOnlyRole(testRole)).toBe(true); + expect(isRoleReadOnly(testRole)).toBe(true); }); test('returns false for disabled roles', () => { @@ -80,12 +133,12 @@ describe('role', () => { enabled: false, }, }; - expect(isReadOnlyRole(testRole)).toBe(false); + expect(isRoleReadOnly(testRole)).toBe(false); }); test('returns false for all other roles', () => { const testRole = {}; - expect(isReadOnlyRole(testRole)).toBe(false); + expect(isRoleReadOnly(testRole)).toBe(false); }); }); diff --git a/x-pack/plugins/security/common/model/role.ts b/x-pack/plugins/security/common/model/role.ts index 1edcf147262ed..4cc7271eaca13 100644 --- a/x-pack/plugins/security/common/model/role.ts +++ b/x-pack/plugins/security/common/model/role.ts @@ -5,6 +5,7 @@ */ import { cloneDeep } from 'lodash'; +import { i18n } from '@kbn/i18n'; import { FeaturesPrivileges } from './features_privileges'; export interface RoleIndexPrivilege { @@ -57,17 +58,41 @@ export function isRoleEnabled(role: Partial) { * * @param role Role as returned by roles API */ -export function isReservedRole(role: Partial) { +export function isRoleReserved(role: Partial) { return (role.metadata?._reserved as boolean) ?? false; } +/** + * Returns whether given role is deprecated or not. + * + * @param {role} the Role as returned by roles API + */ +export function isRoleDeprecated(role: Partial) { + return role.metadata?._deprecated ?? false; +} + +/** + * Returns the extended deprecation notice for the provided role. + * + * @param role the Role as returned by roles API + */ +export function getExtendedRoleDeprecationNotice(role: Partial) { + return i18n.translate('xpack.security.common.extendedRoleDeprecationNotice', { + defaultMessage: `The {roleName} role is deprecated. {reason}`, + values: { + roleName: role.name, + reason: getRoleDeprecatedReason(role), + }, + }); +} + /** * Returns whether given role is editable through the UI or not. * * @param role the Role as returned by roles API */ -export function isReadOnlyRole(role: Partial): boolean { - return isReservedRole(role) || (role._transform_error?.length ?? 0) > 0; +export function isRoleReadOnly(role: Partial): boolean { + return isRoleReserved(role) || (role._transform_error?.length ?? 0) > 0; } /** @@ -91,3 +116,12 @@ export function prepareRoleClone(role: Role): Role { return clone; } + +/** + * Returns the reason this role is deprecated. + * + * @param role the Role as returned by roles API + */ +function getRoleDeprecatedReason(role: Partial) { + return role.metadata?._deprecated_reason ?? ''; +} diff --git a/x-pack/plugins/security/public/account_management/account_management_page.test.tsx b/x-pack/plugins/security/public/account_management/account_management_page.test.tsx index 46bbedd37c434..e58d8e8421547 100644 --- a/x-pack/plugins/security/public/account_management/account_management_page.test.tsx +++ b/x-pack/plugins/security/public/account_management/account_management_page.test.tsx @@ -48,7 +48,7 @@ describe('', () => { ); @@ -70,7 +70,7 @@ describe('', () => { ); @@ -88,7 +88,7 @@ describe('', () => { ); @@ -106,7 +106,7 @@ describe('', () => { ); @@ -125,7 +125,7 @@ describe('', () => { ); diff --git a/x-pack/plugins/security/public/account_management/account_management_page.tsx b/x-pack/plugins/security/public/account_management/account_management_page.tsx index 3f764adc7949f..9388c2e9b19b8 100644 --- a/x-pack/plugins/security/public/account_management/account_management_page.tsx +++ b/x-pack/plugins/security/public/account_management/account_management_page.tsx @@ -14,11 +14,11 @@ import { PersonalInfo } from './personal_info'; interface Props { authc: AuthenticationServiceSetup; - apiClient: PublicMethodsOf; + userAPIClient: PublicMethodsOf; notifications: NotificationsStart; } -export const AccountManagementPage = ({ apiClient, authc, notifications }: Props) => { +export const AccountManagementPage = ({ userAPIClient, authc, notifications }: Props) => { const [currentUser, setCurrentUser] = useState(null); useEffect(() => { authc.getCurrentUser().then(setCurrentUser); @@ -40,7 +40,11 @@ export const AccountManagementPage = ({ apiClient, authc, notifications }: Props - + diff --git a/x-pack/plugins/security/public/account_management/change_password/change_password.tsx b/x-pack/plugins/security/public/account_management/change_password/change_password.tsx index f5ac5f3b21d2e..5b27df24f975c 100644 --- a/x-pack/plugins/security/public/account_management/change_password/change_password.tsx +++ b/x-pack/plugins/security/public/account_management/change_password/change_password.tsx @@ -13,7 +13,7 @@ import { ChangePasswordForm } from '../../management/users/components/change_pas interface Props { user: AuthenticatedUser; - apiClient: PublicMethodsOf; + userAPIClient: PublicMethodsOf; notifications: NotificationsSetup; } @@ -48,7 +48,7 @@ export class ChangePassword extends Component { diff --git a/x-pack/plugins/security/public/management/badges/deprecated_badge.tsx b/x-pack/plugins/security/public/management/badges/deprecated_badge.tsx new file mode 100644 index 0000000000000..63c38b4f3a828 --- /dev/null +++ b/x-pack/plugins/security/public/management/badges/deprecated_badge.tsx @@ -0,0 +1,28 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import { EuiBadge, EuiToolTipProps } from '@elastic/eui'; +import { OptionalToolTip } from './optional_tooltip'; + +interface Props { + 'data-test-subj'?: string; + tooltipContent?: EuiToolTipProps['content']; +} + +export const DeprecatedBadge = (props: Props) => { + return ( + + + + + + ); +}; diff --git a/x-pack/plugins/security/public/management/badges/disabled_badge.tsx b/x-pack/plugins/security/public/management/badges/disabled_badge.tsx new file mode 100644 index 0000000000000..a1b851e8c28a3 --- /dev/null +++ b/x-pack/plugins/security/public/management/badges/disabled_badge.tsx @@ -0,0 +1,25 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import { EuiBadge, EuiToolTipProps } from '@elastic/eui'; +import { OptionalToolTip } from './optional_tooltip'; + +interface Props { + 'data-test-subj'?: string; + tooltipContent?: EuiToolTipProps['content']; +} + +export const DisabledBadge = (props: Props) => { + return ( + + + + + + ); +}; diff --git a/x-pack/plugins/security/public/management/badges/enabled_badge.tsx b/x-pack/plugins/security/public/management/badges/enabled_badge.tsx new file mode 100644 index 0000000000000..4c7d3d6dd596c --- /dev/null +++ b/x-pack/plugins/security/public/management/badges/enabled_badge.tsx @@ -0,0 +1,25 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import { EuiBadge, EuiToolTipProps } from '@elastic/eui'; +import { OptionalToolTip } from './optional_tooltip'; + +interface Props { + 'data-test-subj'?: string; + tooltipContent?: EuiToolTipProps['content']; +} + +export const EnabledBadge = (props: Props) => { + return ( + + + + + + ); +}; diff --git a/x-pack/plugins/security/public/management/badges/index.ts b/x-pack/plugins/security/public/management/badges/index.ts new file mode 100644 index 0000000000000..b29bac6f0928a --- /dev/null +++ b/x-pack/plugins/security/public/management/badges/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { DeprecatedBadge } from './deprecated_badge'; +export { DisabledBadge } from './disabled_badge'; +export { EnabledBadge } from './enabled_badge'; +export { ReservedBadge } from './reserved_badge'; diff --git a/x-pack/plugins/security/public/management/badges/optional_tooltip.tsx b/x-pack/plugins/security/public/management/badges/optional_tooltip.tsx new file mode 100644 index 0000000000000..4c412396ac7ec --- /dev/null +++ b/x-pack/plugins/security/public/management/badges/optional_tooltip.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { ReactElement } from 'react'; +import { EuiToolTipProps, EuiToolTip } from '@elastic/eui'; + +interface Props { + children: ReactElement; + tooltipContent?: EuiToolTipProps['content']; +} +export const OptionalToolTip = (props: Props) => { + if (props.tooltipContent) { + return {props.children}; + } + return props.children; +}; diff --git a/x-pack/plugins/security/public/management/badges/reserved_badge.tsx b/x-pack/plugins/security/public/management/badges/reserved_badge.tsx new file mode 100644 index 0000000000000..603e3fa372aec --- /dev/null +++ b/x-pack/plugins/security/public/management/badges/reserved_badge.tsx @@ -0,0 +1,25 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import { EuiBadge, EuiToolTipProps } from '@elastic/eui'; +import { OptionalToolTip } from './optional_tooltip'; + +interface Props { + 'data-test-subj'?: string; + tooltipContent?: EuiToolTipProps['content']; +} + +export const ReservedBadge = (props: Props) => { + return ( + + + + + + ); +}; diff --git a/x-pack/plugins/ml/server/lib/check_license/index.ts b/x-pack/plugins/security/public/management/role_combo_box/index.ts similarity index 83% rename from x-pack/plugins/ml/server/lib/check_license/index.ts rename to x-pack/plugins/security/public/management/role_combo_box/index.ts index f2c070fd44b6e..b7c827a22205f 100644 --- a/x-pack/plugins/ml/server/lib/check_license/index.ts +++ b/x-pack/plugins/security/public/management/role_combo_box/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { checkLicense } from './check_license'; +export { RoleComboBox } from './role_combo_box'; diff --git a/x-pack/plugins/security/public/management/role_combo_box/role_combo_box.test.tsx b/x-pack/plugins/security/public/management/role_combo_box/role_combo_box.test.tsx new file mode 100644 index 0000000000000..6a041144d0b6a --- /dev/null +++ b/x-pack/plugins/security/public/management/role_combo_box/role_combo_box.test.tsx @@ -0,0 +1,110 @@ +/* + * 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 { mountWithIntl } from 'test_utils/enzyme_helpers'; + +import { RoleComboBox } from '.'; +import { EuiComboBox } from '@elastic/eui'; +import { findTestSubject } from 'test_utils/find_test_subject'; + +describe('RoleComboBox', () => { + it('renders the provided list of roles via EuiComboBox options', () => { + const availableRoles = [ + { + name: 'role-1', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [], + metadata: {}, + }, + { + name: 'role-2', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [], + metadata: {}, + }, + ]; + const wrapper = mountWithIntl( + + ); + + expect(wrapper.find(EuiComboBox).props().options).toMatchInlineSnapshot(` + Array [ + Object { + "color": "default", + "data-test-subj": "roleOption-role-1", + "label": "role-1", + "value": Object { + "isDeprecated": false, + }, + }, + Object { + "color": "default", + "data-test-subj": "roleOption-role-2", + "label": "role-2", + "value": Object { + "isDeprecated": false, + }, + }, + ] + `); + }); + + it('renders deprecated roles as such', () => { + const availableRoles = [ + { + name: 'role-1', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [], + metadata: { _deprecated: true }, + }, + ]; + const wrapper = mountWithIntl( + + ); + + expect(wrapper.find(EuiComboBox).props().options).toMatchInlineSnapshot(` + Array [ + Object { + "color": "warning", + "data-test-subj": "roleOption-role-1", + "label": "role-1", + "value": Object { + "isDeprecated": true, + }, + }, + ] + `); + }); + + it('renders the selected role names in the expanded list, coded according to deprecated status', () => { + const availableRoles = [ + { + name: 'role-1', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [], + metadata: {}, + }, + { + name: 'role-2', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [], + metadata: {}, + }, + ]; + const wrapper = mountWithIntl( +
+ +
+ ); + + findTestSubject(wrapper, 'comboBoxToggleListButton').simulate('click'); + + wrapper.find(EuiComboBox).setState({ isListOpen: true }); + + expect(findTestSubject(wrapper, 'rolesDropdown-renderOption')).toMatchInlineSnapshot(`null`); + }); +}); diff --git a/x-pack/plugins/security/public/management/role_combo_box/role_combo_box.tsx b/x-pack/plugins/security/public/management/role_combo_box/role_combo_box.tsx new file mode 100644 index 0000000000000..65fd8a8324a7d --- /dev/null +++ b/x-pack/plugins/security/public/management/role_combo_box/role_combo_box.tsx @@ -0,0 +1,61 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { EuiComboBox } from '@elastic/eui'; +import { Role, isRoleDeprecated } from '../../../common/model'; +import { RoleComboBoxOption } from './role_combo_box_option'; + +interface Props { + availableRoles: Role[]; + selectedRoleNames: string[]; + onChange: (selectedRoleNames: string[]) => void; + placeholder?: string; + isLoading?: boolean; + isDisabled?: boolean; +} + +export const RoleComboBox = (props: Props) => { + const onRolesChange = (selectedItems: Array<{ label: string }>) => { + props.onChange(selectedItems.map(item => item.label)); + }; + + const roleNameToOption = (roleName: string) => { + const roleDefinition = props.availableRoles.find(role => role.name === roleName); + const isDeprecated: boolean = (roleDefinition && isRoleDeprecated(roleDefinition)) ?? false; + return { + color: isDeprecated ? 'warning' : 'default', + 'data-test-subj': `roleOption-${roleName}`, + label: roleName, + value: { + isDeprecated, + }, + }; + }; + + const options = props.availableRoles.map(role => roleNameToOption(role.name)); + + const selectedOptions = props.selectedRoleNames.map(roleNameToOption); + + return ( + } + /> + ); +}; diff --git a/x-pack/plugins/security/public/management/role_combo_box/role_combo_box_option.test.tsx b/x-pack/plugins/security/public/management/role_combo_box/role_combo_box_option.test.tsx new file mode 100644 index 0000000000000..c1ac381ba9994 --- /dev/null +++ b/x-pack/plugins/security/public/management/role_combo_box/role_combo_box_option.test.tsx @@ -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 React from 'react'; +import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { RoleComboBoxOption } from './role_combo_box_option'; + +describe('RoleComboBoxOption', () => { + it('renders a regular role correctly', () => { + const wrapper = shallowWithIntl( + + ); + + expect(wrapper).toMatchInlineSnapshot(` + + role-1 + + + `); + }); + + it('renders a deprecated role correctly', () => { + const wrapper = shallowWithIntl( + + ); + + expect(wrapper).toMatchInlineSnapshot(` + + role-1 + + (deprecated) + + `); + }); +}); diff --git a/x-pack/plugins/security/public/management/role_combo_box/role_combo_box_option.tsx b/x-pack/plugins/security/public/management/role_combo_box/role_combo_box_option.tsx new file mode 100644 index 0000000000000..126a3151adf01 --- /dev/null +++ b/x-pack/plugins/security/public/management/role_combo_box/role_combo_box_option.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 React from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { EuiComboBoxOptionProps, EuiText } from '@elastic/eui'; + +interface Props { + option: EuiComboBoxOptionProps<{ isDeprecated: boolean }>; +} + +export const RoleComboBoxOption = ({ option }: Props) => { + const isDeprecated = option.value?.isDeprecated ?? false; + const deprecatedLabel = i18n.translate( + 'xpack.security.management.users.editUser.deprecatedRoleText', + { + defaultMessage: '(deprecated)', + } + ); + + return ( + + {option.label} {isDeprecated ? deprecatedLabel : ''} + + ); +}; diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.test.tsx index 96bc81c8cd4d0..149c1271123d2 100644 --- a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.test.tsx @@ -17,7 +17,6 @@ import { EditRoleMappingPage } from '.'; import { NoCompatibleRealms, SectionLoading, PermissionDenied } from '../components'; import { VisualRuleEditor } from './rule_editor_panel/visual_rule_editor'; import { JSONRuleEditor } from './rule_editor_panel/json_rule_editor'; -import { EuiComboBox } from '@elastic/eui'; import { RolesAPIClient } from '../../roles'; import { Role } from '../../../../common/model'; import { DocumentationLinksService } from '../documentation_links'; @@ -25,6 +24,7 @@ import { DocumentationLinksService } from '../documentation_links'; import { coreMock } from '../../../../../../../src/core/public/mocks'; import { roleMappingsAPIClientMock } from '../role_mappings_api_client.mock'; import { rolesAPIClientMock } from '../../roles/roles_api_client.mock'; +import { RoleComboBox } from '../../role_combo_box'; describe('EditRoleMappingPage', () => { let rolesAPI: PublicMethodsOf; @@ -33,6 +33,7 @@ describe('EditRoleMappingPage', () => { (rolesAPI as jest.Mocked).getRoles.mockResolvedValue([ { name: 'foo_role' }, { name: 'bar role' }, + { name: 'some-deprecated-role', metadata: { _deprecated: true } }, ] as Role[]); }); @@ -63,10 +64,10 @@ describe('EditRoleMappingPage', () => { target: { value: 'my-role-mapping' }, }); - (wrapper - .find(EuiComboBox) - .filter('[data-test-subj="roleMappingFormRoleComboBox"]') - .props() as any).onChange([{ label: 'foo_role' }]); + wrapper + .find(RoleComboBox) + .props() + .onChange(['foo_role']); findTestSubject(wrapper, 'roleMappingsAddRuleButton').simulate('click'); @@ -126,10 +127,10 @@ describe('EditRoleMappingPage', () => { findTestSubject(wrapper, 'switchToRolesButton').simulate('click'); - (wrapper - .find(EuiComboBox) - .filter('[data-test-subj="roleMappingFormRoleComboBox"]') - .props() as any).onChange([{ label: 'foo_role' }]); + wrapper + .find(RoleComboBox) + .props() + .onChange(['foo_role']); findTestSubject(wrapper, 'roleMappingsAddRuleButton').simulate('click'); wrapper.find('button[id="addRuleOption"]').simulate('click'); @@ -207,6 +208,42 @@ describe('EditRoleMappingPage', () => { expect(wrapper.find(NoCompatibleRealms)).toHaveLength(1); }); + it('renders a message when editing a mapping with deprecated roles assigned', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.getRoleMapping.mockResolvedValue({ + name: 'foo', + roles: ['some-deprecated-role'], + enabled: true, + rules: { + field: { username: '*' }, + }, + }); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + canUseInlineScripts: true, + canUseStoredScripts: true, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + + expect(findTestSubject(wrapper, 'deprecatedRolesAssigned')).toHaveLength(0); + + await nextTick(); + wrapper.update(); + + expect(findTestSubject(wrapper, 'deprecatedRolesAssigned')).toHaveLength(1); + }); + it('renders a warning when editing a mapping with a stored role template, when stored scripts are disabled', async () => { const roleMappingsAPI = roleMappingsAPIClientMock.create(); roleMappingsAPI.getRoleMapping.mockResolvedValue({ diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.tsx index 02af6bfbafa7e..b376a3943ff48 100644 --- a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.tsx @@ -17,6 +17,7 @@ import { EuiIcon, EuiSwitch, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { RoleMapping } from '../../../../../common/model'; import { RolesAPIClient } from '../../../roles'; @@ -276,12 +277,12 @@ export class MappingInfoPanel extends Component { > - } + label={i18n.translate( + 'xpack.security.management.editRoleMapping.roleMappingEnabledLabel', + { + defaultMessage: 'Enable mapping', + } + )} showLabel={false} data-test-subj="roleMappingsEnabledSwitch" checked={this.props.roleMapping.enabled} diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_selector.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_selector.tsx index 992c2741ae93e..8e1597cf3d598 100644 --- a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_selector.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_selector.tsx @@ -6,11 +6,13 @@ import React, { Fragment } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiComboBox, EuiFormRow, EuiHorizontalRule } from '@elastic/eui'; -import { RoleMapping, Role } from '../../../../../common/model'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFormRow, EuiHorizontalRule } from '@elastic/eui'; +import { RoleMapping, Role, isRoleDeprecated } from '../../../../../common/model'; import { RolesAPIClient } from '../../../roles'; import { AddRoleTemplateButton } from './add_role_template_button'; import { RoleTemplateEditor } from './role_template_editor'; +import { RoleComboBox } from '../../../role_combo_box'; interface Props { rolesAPIClient: PublicMethodsOf; @@ -40,7 +42,7 @@ export class RoleSelector extends React.Component { public render() { const { mode } = this.props; return ( - + {mode === 'roles' ? this.getRoleComboBox() : this.getRoleTemplates()} ); @@ -49,19 +51,18 @@ export class RoleSelector extends React.Component { private getRoleComboBox = () => { const { roles = [] } = this.props.roleMapping; return ( - ({ label: r.name }))} - selectedOptions={roles!.map(r => ({ label: r }))} - onChange={selectedOptions => { + availableRoles={this.state.roles} + selectedRoleNames={roles} + onChange={selectedRoles => { this.props.onChange({ ...this.props.roleMapping, - roles: selectedOptions.map(so => so.label), + roles: selectedRoles, role_templates: [], }); }} @@ -130,4 +131,25 @@ export class RoleSelector extends React.Component { ); }; + + private getHelpText = () => { + if (this.props.mode === 'roles' && this.hasDeprecatedRolesAssigned()) { + return ( + + + + ); + } + }; + + private hasDeprecatedRolesAssigned = () => { + return ( + this.props.roleMapping.roles?.some(r => + this.state.roles.some(role => role.name === r && isRoleDeprecated(role)) + ) ?? false + ); + }; } diff --git a/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.test.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.test.tsx index de0722b4cd85e..0d343ad33d78e 100644 --- a/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.test.tsx @@ -16,6 +16,7 @@ import { DocumentationLinksService } from '../documentation_links'; import { coreMock } from '../../../../../../../src/core/public/mocks'; import { roleMappingsAPIClientMock } from '../role_mappings_api_client.mock'; +import { rolesAPIClientMock } from '../../roles/index.mock'; describe('RoleMappingsGridPage', () => { it('renders an empty prompt when no role mappings exist', async () => { @@ -29,6 +30,7 @@ describe('RoleMappingsGridPage', () => { const { docLinks, notifications } = coreMock.createStart(); const wrapper = mountWithIntl( { const { docLinks, notifications } = coreMock.createStart(); const wrapper = mountWithIntl( { const { docLinks, notifications } = coreMock.createStart(); const wrapper = mountWithIntl( { expect(wrapper.find(NoCompatibleRealms)).toHaveLength(1); }); - it('renders links to mapped roles', async () => { + it('renders links to mapped roles, even if the roles API call returns nothing', async () => { const roleMappingsAPI = roleMappingsAPIClientMock.create(); roleMappingsAPI.getRoleMappings.mockResolvedValue([ { @@ -122,6 +126,7 @@ describe('RoleMappingsGridPage', () => { const { docLinks, notifications } = coreMock.createStart(); const wrapper = mountWithIntl( { const { docLinks, notifications } = coreMock.createStart(); const wrapper = mountWithIntl( { const { docLinks, notifications } = coreMock.createStart(); const wrapper = mountWithIntl( { // Expect an additional API call to refresh the grid expect(roleMappingsAPI.getRoleMappings).toHaveBeenCalledTimes(2); }); + + it('renders a warning when a mapping is assigned a deprecated role', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.getRoleMappings.mockResolvedValue([ + { + name: 'some-realm', + enabled: true, + roles: ['superuser', 'kibana_user'], + rules: { field: { username: '*' } }, + }, + ]); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + }); + roleMappingsAPI.deleteRoleMappings.mockResolvedValue([ + { + name: 'some-realm', + success: true, + }, + ]); + + const roleAPIClient = rolesAPIClientMock.create(); + roleAPIClient.getRoles.mockResolvedValue([ + { + name: 'kibana_user', + metadata: { + _deprecated: true, + _deprecated_reason: `I don't like you.`, + }, + }, + ]); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + await nextTick(); + wrapper.update(); + + const deprecationTooltip = wrapper.find('[data-test-subj="roleDeprecationTooltip"]').props(); + + expect(deprecationTooltip).toMatchInlineSnapshot(` + Object { + "children":
+ kibana_user + + +
, + "content": "The kibana_user role is deprecated. I don't like you.", + "data-test-subj": "roleDeprecationTooltip", + "delay": "regular", + "position": "top", + } + `); + }); }); diff --git a/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.tsx index feb918cb6b301..5802c3444e85f 100644 --- a/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.tsx @@ -6,7 +6,6 @@ import React, { Component, Fragment } from 'react'; import { - EuiBadge, EuiButton, EuiButtonIcon, EuiCallOut, @@ -26,7 +25,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { NotificationsStart } from 'src/core/public'; -import { RoleMapping } from '../../../../common/model'; +import { RoleMapping, Role } from '../../../../common/model'; import { EmptyPrompt } from './empty_prompt'; import { NoCompatibleRealms, @@ -34,15 +33,15 @@ import { PermissionDenied, SectionLoading, } from '../components'; -import { - getCreateRoleMappingHref, - getEditRoleMappingHref, - getEditRoleHref, -} from '../../management_urls'; +import { getCreateRoleMappingHref, getEditRoleMappingHref } from '../../management_urls'; import { DocumentationLinksService } from '../documentation_links'; import { RoleMappingsAPIClient } from '../role_mappings_api_client'; +import { RoleTableDisplay } from '../../role_table_display'; +import { RolesAPIClient } from '../../roles'; +import { EnabledBadge, DisabledBadge } from '../../badges'; interface Props { + rolesAPIClient: PublicMethodsOf; roleMappingsAPI: PublicMethodsOf; notifications: NotificationsStart; docLinks: DocumentationLinksService; @@ -51,6 +50,7 @@ interface Props { interface State { loadState: 'loadingApp' | 'loadingTable' | 'permissionDenied' | 'finished'; roleMappings: null | RoleMapping[]; + roles: null | Role[]; selectedItems: RoleMapping[]; hasCompatibleRealms: boolean; error: any; @@ -62,6 +62,7 @@ export class RoleMappingsGridPage extends Component { this.state = { loadState: 'loadingApp', roleMappings: null, + roles: null, hasCompatibleRealms: true, selectedItems: [], error: undefined, @@ -308,7 +309,7 @@ export class RoleMappingsGridPage extends Component { }), sortable: true, render: (entry: any, record: RoleMapping) => { - const { roles = [], role_templates: roleTemplates = [] } = record; + const { roles: assignedRoleNames = [], role_templates: roleTemplates = [] } = record; if (roleTemplates.length > 0) { return ( @@ -322,13 +323,11 @@ export class RoleMappingsGridPage extends Component { ); } - const roleLinks = roles.map((rolename, index) => { - return ( - - {rolename} - {index === roles.length - 1 ? null : ', '} - - ); + const roleLinks = assignedRoleNames.map((rolename, index) => { + const role: Role | string = + this.state.roles?.find(r => r.name === rolename) ?? rolename; + + return ; }); return
{roleLinks}
; }, @@ -341,24 +340,10 @@ export class RoleMappingsGridPage extends Component { sortable: true, render: (enabled: boolean) => { if (enabled) { - return ( - - - - ); + return ; } - return ( - - - - ); + return ; }, }, { @@ -458,13 +443,27 @@ export class RoleMappingsGridPage extends Component { }); if (canManageRoleMappings) { - this.loadRoleMappings(); + this.performInitialLoad(); } } catch (e) { this.setState({ error: e, loadState: 'finished' }); } } + private performInitialLoad = async () => { + try { + const [roleMappings, roles] = await Promise.all([ + this.props.roleMappingsAPI.getRoleMappings(), + this.props.rolesAPIClient.getRoles(), + ]); + this.setState({ roleMappings, roles }); + } catch (e) { + this.setState({ error: e }); + } + + this.setState({ loadState: 'finished' }); + }; + private reloadRoleMappings = () => { this.setState({ roleMappings: [], loadState: 'loadingTable' }); this.loadRoleMappings(); diff --git a/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.test.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.test.tsx index 9c41d6624065e..5907413d7299e 100644 --- a/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.test.tsx @@ -53,7 +53,7 @@ describe('roleMappingsManagementApp', () => { expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: `#${basePath}`, text: 'Role Mappings' }]); expect(container).toMatchInlineSnapshot(`
- Role Mappings Page: {"notifications":{"toasts":{}},"roleMappingsAPI":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"}} + Role Mappings Page: {"notifications":{"toasts":{}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"roleMappingsAPI":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"}}
`); diff --git a/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx index af1572cedbade..8e1ac8d7f6957 100644 --- a/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx @@ -48,6 +48,7 @@ export const roleMappingsManagementApp = Object.freeze({ return ( diff --git a/x-pack/plugins/security/public/management/role_table_display/index.ts b/x-pack/plugins/security/public/management/role_table_display/index.ts new file mode 100644 index 0000000000000..71f100ee68bfa --- /dev/null +++ b/x-pack/plugins/security/public/management/role_table_display/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { RoleTableDisplay } from './role_table_display'; diff --git a/x-pack/plugins/security/public/management/role_table_display/role_table_display.tsx b/x-pack/plugins/security/public/management/role_table_display/role_table_display.tsx new file mode 100644 index 0000000000000..28978f0090011 --- /dev/null +++ b/x-pack/plugins/security/public/management/role_table_display/role_table_display.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiLink, EuiToolTip, EuiIcon } from '@elastic/eui'; +import { Role, isRoleDeprecated, getExtendedRoleDeprecationNotice } from '../../../common/model'; +import { getEditRoleHref } from '../management_urls'; + +interface Props { + role: Role | string; +} + +export const RoleTableDisplay = ({ role }: Props) => { + let content; + let href; + if (typeof role === 'string') { + content =
{role}
; + href = getEditRoleHref(role); + } else if (isRoleDeprecated(role)) { + content = ( + +
+ {role.name} +
+
+ ); + href = getEditRoleHref(role.name); + } else { + content =
{role.name}
; + href = getEditRoleHref(role.name); + } + return {content}; +}; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx index 42ec3fa419167..cd7766ef38748 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx @@ -17,6 +17,7 @@ import { EuiSpacer, EuiText, EuiTitle, + EuiCallOut, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -44,11 +45,13 @@ import { RawKibanaPrivileges, Role, BuiltinESPrivileges, - isReadOnlyRole as checkIfRoleReadOnly, - isReservedRole as checkIfRoleReserved, + isRoleReadOnly as checkIfRoleReadOnly, + isRoleReserved as checkIfRoleReserved, + isRoleDeprecated as checkIfRoleDeprecated, copyRole, prepareRoleClone, RoleIndexPrivilege, + getExtendedRoleDeprecationNotice, } from '../../../../common/model'; import { ROLES_PATH } from '../../management_urls'; import { RoleValidationResult, RoleValidator } from './validate_role'; @@ -299,8 +302,9 @@ export const EditRolePage: FunctionComponent = ({ } const isEditingExistingRole = !!roleName && action === 'edit'; - const isReadOnlyRole = checkIfRoleReadOnly(role); - const isReservedRole = checkIfRoleReserved(role); + const isRoleReadOnly = checkIfRoleReadOnly(role); + const isRoleReserved = checkIfRoleReserved(role); + const isDeprecatedRole = checkIfRoleDeprecated(role); const [kibanaPrivileges, builtInESPrivileges] = privileges; @@ -309,7 +313,7 @@ export const EditRolePage: FunctionComponent = ({ const props: HTMLProps = { tabIndex: 0, }; - if (isReservedRole) { + if (isRoleReserved) { titleText = ( = ({ }; const getActionButton = () => { - if (isEditingExistingRole && !isReadOnlyRole) { + if (isEditingExistingRole && !isRoleReadOnly) { return ( @@ -365,7 +369,7 @@ export const EditRolePage: FunctionComponent = ({ /> } helpText={ - !isReservedRole && isEditingExistingRole ? ( + !isRoleReserved && isEditingExistingRole ? ( = ({ value={role.name || ''} onChange={onNameChange} data-test-subj={'roleFormNameInput'} - readOnly={isReservedRole || isEditingExistingRole} + readOnly={isRoleReserved || isEditingExistingRole} />
@@ -400,7 +404,7 @@ export const EditRolePage: FunctionComponent = ({ = ({ spacesEnabled={spacesEnabled} features={features} uiCapabilities={uiCapabilities} - editable={!isReadOnlyRole} + editable={!isRoleReadOnly} role={role} onChange={onRoleChange} validator={validator} @@ -436,7 +440,7 @@ export const EditRolePage: FunctionComponent = ({ }; const getFormButtons = () => { - if (isReadOnlyRole) { + if (isRoleReadOnly) { return getReturnToRoleListButton(); } @@ -479,7 +483,7 @@ export const EditRolePage: FunctionComponent = ({ data-test-subj={`roleFormSaveButton`} fill onClick={saveRole} - disabled={isReservedRole} + disabled={isRoleReserved} > {saveText} @@ -563,7 +567,7 @@ export const EditRolePage: FunctionComponent = ({ {description} - {isReservedRole && ( + {isRoleReserved && ( @@ -577,6 +581,17 @@ export const EditRolePage: FunctionComponent = ({ )} + {isDeprecatedRole && ( + + + + + )} + {getRoleName()} diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/cluster_privileges.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/cluster_privileges.tsx index 380d54733ce0e..54be04ade370e 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/cluster_privileges.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/cluster_privileges.tsx @@ -7,7 +7,7 @@ import { EuiComboBox, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React, { Component } from 'react'; import _ from 'lodash'; -import { Role, isReadOnlyRole } from '../../../../../../common/model'; +import { Role, isRoleReadOnly } from '../../../../../../common/model'; interface Props { role: Role; @@ -38,7 +38,7 @@ export class ClusterPrivileges extends Component { selectedOptions={selectedOptions} onChange={this.onClusterPrivilegesChange} onCreateOption={this.onCreateCustomPrivilege} - isDisabled={isReadOnlyRole(role)} + isDisabled={isRoleReadOnly(role)} /> ); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.test.tsx index 5e2da51314365..879cd8e2759ab 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.test.tsx @@ -23,7 +23,7 @@ test('it renders without crashing', () => { indexPatterns: [], availableFields: [], availableIndexPrivileges: ['all', 'read', 'write', 'index'], - isReadOnlyRole: false, + isRoleReadOnly: false, allowDocumentLevelSecurity: true, allowFieldLevelSecurity: true, validator: new RoleValidator(), @@ -50,7 +50,7 @@ describe('delete button', () => { indexPatterns: [], availableFields: [], availableIndexPrivileges: ['all', 'read', 'write', 'index'], - isReadOnlyRole: false, + isRoleReadOnly: false, allowDocumentLevelSecurity: true, allowFieldLevelSecurity: true, validator: new RoleValidator(), @@ -59,19 +59,19 @@ describe('delete button', () => { intl: {} as any, }; - test('it is hidden when isReadOnlyRole is true', () => { + test('it is hidden when isRoleReadOnly is true', () => { const testProps = { ...props, - isReadOnlyRole: true, + isRoleReadOnly: true, }; const wrapper = mountWithIntl(); expect(wrapper.find(EuiButtonIcon)).toHaveLength(0); }); - test('it is shown when isReadOnlyRole is false', () => { + test('it is shown when isRoleReadOnly is false', () => { const testProps = { ...props, - isReadOnlyRole: false, + isRoleReadOnly: false, }; const wrapper = mountWithIntl(); expect(wrapper.find(EuiButtonIcon)).toHaveLength(1); @@ -80,7 +80,7 @@ describe('delete button', () => { test('it invokes onDelete when clicked', () => { const testProps = { ...props, - isReadOnlyRole: false, + isRoleReadOnly: false, }; const wrapper = mountWithIntl(); wrapper.find(EuiButtonIcon).simulate('click'); @@ -102,7 +102,7 @@ describe(`document level security`, () => { indexPatterns: [], availableFields: [], availableIndexPrivileges: ['all', 'read', 'write', 'index'], - isReadOnlyRole: false, + isRoleReadOnly: false, allowDocumentLevelSecurity: true, allowFieldLevelSecurity: true, validator: new RoleValidator(), @@ -161,7 +161,7 @@ describe('field level security', () => { indexPatterns: [], availableFields: [], availableIndexPrivileges: ['all', 'read', 'write', 'index'], - isReadOnlyRole: false, + isRoleReadOnly: false, allowDocumentLevelSecurity: true, allowFieldLevelSecurity: true, validator: new RoleValidator(), diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.tsx index 15e0367c2b6dc..b5d0a2c91d1be 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.tsx @@ -33,7 +33,7 @@ interface Props { availableFields: string[]; onChange: (indexPrivilege: RoleIndexPrivilege) => void; onDelete: () => void; - isReadOnlyRole: boolean; + isRoleReadOnly: boolean; allowDocumentLevelSecurity: boolean; allowFieldLevelSecurity: boolean; validator: RoleValidator; @@ -68,7 +68,7 @@ export class IndexPrivilegeForm extends Component { {this.getPrivilegeForm()} - {!this.props.isReadOnlyRole && ( + {!this.props.isRoleReadOnly && ( { selectedOptions={this.props.indexPrivilege.names.map(toOption)} onCreateOption={this.onCreateIndexPatternOption} onChange={this.onIndexPatternsChange} - isDisabled={this.props.isReadOnlyRole} + isDisabled={this.props.isRoleReadOnly} /> @@ -128,7 +128,7 @@ export class IndexPrivilegeForm extends Component { options={this.props.availableIndexPrivileges.map(toOption)} selectedOptions={this.props.indexPrivilege.privileges.map(toOption)} onChange={this.onPrivilegeChange} - isDisabled={this.props.isReadOnlyRole} + isDisabled={this.props.isRoleReadOnly} /> @@ -149,7 +149,7 @@ export class IndexPrivilegeForm extends Component { allowDocumentLevelSecurity, availableFields, indexPrivilege, - isReadOnlyRole, + isRoleReadOnly, } = this.props; if (!allowFieldLevelSecurity) { @@ -161,7 +161,7 @@ export class IndexPrivilegeForm extends Component { return ( <> - {!isReadOnlyRole && ( + {!isRoleReadOnly && ( { { fullWidth={true} className="indexPrivilegeForm__grantedFieldsRow" helpText={ - !isReadOnlyRole && grant.length === 0 ? ( + !isRoleReadOnly && grant.length === 0 ? ( { selectedOptions={grant.map(toOption)} onCreateOption={this.onCreateGrantedField} onChange={this.onGrantedFieldsChange} - isDisabled={this.props.isReadOnlyRole} + isDisabled={this.props.isRoleReadOnly} /> @@ -233,7 +233,7 @@ export class IndexPrivilegeForm extends Component { selectedOptions={except.map(toOption)} onCreateOption={this.onCreateDeniedField} onChange={this.onDeniedFieldsChange} - isDisabled={isReadOnlyRole} + isDisabled={isRoleReadOnly} /> @@ -248,7 +248,7 @@ export class IndexPrivilegeForm extends Component { }; private getGrantedDocumentsControl = () => { - const { allowDocumentLevelSecurity, indexPrivilege, isReadOnlyRole } = this.props; + const { allowDocumentLevelSecurity, indexPrivilege, isRoleReadOnly } = this.props; if (!allowDocumentLevelSecurity) { return null; @@ -256,7 +256,7 @@ export class IndexPrivilegeForm extends Component { return ( - {!this.props.isReadOnlyRole && ( + {!this.props.isRoleReadOnly && ( { { compressed={true} checked={this.state.queryExpanded} onChange={this.toggleDocumentQuery} - disabled={isReadOnlyRole} + disabled={isRoleReadOnly} /> } @@ -292,7 +292,7 @@ export class IndexPrivilegeForm extends Component { fullWidth={true} value={indexPrivilege.query} onChange={this.onQueryChange} - readOnly={this.props.isReadOnlyRole} + readOnly={this.props.isRoleReadOnly} /> diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.tsx index 2c745067fede2..1157640ca57a7 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.tsx @@ -8,7 +8,7 @@ import React, { Component, Fragment } from 'react'; import { Role, RoleIndexPrivilege, - isReadOnlyRole, + isRoleReadOnly, isRoleEnabled, } from '../../../../../../common/model'; import { SecurityLicense } from '../../../../../../common/licensing'; @@ -57,7 +57,7 @@ export class IndexPrivileges extends Component { // doesn't permit FLS/DLS). allowDocumentLevelSecurity: allowRoleDocumentLevelSecurity || !isRoleEnabled(this.props.role), allowFieldLevelSecurity: allowRoleFieldLevelSecurity || !isRoleEnabled(this.props.role), - isReadOnlyRole: isReadOnlyRole(this.props.role), + isRoleReadOnly: isRoleReadOnly(this.props.role), }; const forms = indices.map((indexPrivilege: RoleIndexPrivilege, idx) => ( @@ -143,7 +143,7 @@ export class IndexPrivileges extends Component { public loadAvailableFields(privileges: RoleIndexPrivilege[]) { // readonly roles cannot be edited, and therefore do not need to fetch available fields. - if (isReadOnlyRole(this.props.role)) { + if (isRoleReadOnly(this.props.role)) { return; } diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx index 5fc238eed0ae7..a847ccb677485 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx @@ -17,7 +17,7 @@ import React, { Component, Fragment } from 'react'; import { Capabilities } from 'src/core/public'; import { Space } from '../../../../../../../../spaces/public'; import { Feature } from '../../../../../../../../features/public'; -import { KibanaPrivileges, Role, isReservedRole } from '../../../../../../../common/model'; +import { KibanaPrivileges, Role, isRoleReserved } from '../../../../../../../common/model'; import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator'; import { RoleValidator } from '../../../validate_role'; import { PrivilegeMatrix } from './privilege_matrix'; @@ -219,7 +219,7 @@ class SpaceAwarePrivilegeSectionUI extends Component { return ( {addPrivilegeButton} - {hasPrivilegesAssigned && !isReservedRole(this.props.role) && ( + {hasPrivilegesAssigned && !isRoleReserved(this.props.role) && ( {viewMatrixButton} )} diff --git a/x-pack/plugins/security/public/management/roles/edit_role/reserved_role_badge.tsx b/x-pack/plugins/security/public/management/roles/edit_role/reserved_role_badge.tsx index 501ca7589dafd..3a79c400b8d59 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/reserved_role_badge.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/reserved_role_badge.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { EuiIcon, EuiToolTip } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Role, isReservedRole } from '../../../../common/model'; +import { Role, isRoleReserved } from '../../../../common/model'; interface Props { role: Role; @@ -17,7 +17,7 @@ interface Props { export const ReservedRoleBadge = (props: Props) => { const { role } = props; - if (isReservedRole(role)) { + if (isRoleReserved(role)) { return ( ({ body: { statusCode: 403 } }); @@ -76,8 +78,24 @@ describe('', () => { }); expect(wrapper.find(PermissionDenied)).toHaveLength(0); - expect(wrapper.find('EuiIcon[data-test-subj="reservedRole"]')).toHaveLength(1); - expect(wrapper.find('EuiCheckbox[title="Role is reserved"]')).toHaveLength(1); + expect(wrapper.find(ReservedBadge)).toHaveLength(1); + }); + + it(`renders disabled roles as such`, async () => { + const wrapper = mountWithIntl( + + ); + const initialIconCount = wrapper.find(EuiIcon).length; + + await waitForRender(wrapper, updatedWrapper => { + return updatedWrapper.find(EuiIcon).length > initialIconCount; + }); + + expect(wrapper.find(PermissionDenied)).toHaveLength(0); + expect(wrapper.find(DisabledBadge)).toHaveLength(1); }); it('renders permission denied if required', async () => { @@ -123,4 +141,54 @@ describe('', () => { wrapper.find('EuiButtonIcon[data-test-subj="clone-role-action-disabled-role"]') ).toHaveLength(1); }); + + it('hides reserved roles when instructed to', async () => { + const wrapper = mountWithIntl( + + ); + const initialIconCount = wrapper.find(EuiIcon).length; + + await waitForRender(wrapper, updatedWrapper => { + return updatedWrapper.find(EuiIcon).length > initialIconCount; + }); + + expect(wrapper.find(EuiBasicTable).props().items).toEqual([ + { + name: 'disabled-role', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [{ base: [], spaces: [], feature: {} }], + transient_metadata: { enabled: false }, + }, + { + name: 'reserved-role', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [{ base: [], spaces: [], feature: {} }], + metadata: { _reserved: true }, + }, + { + name: 'test-role-1', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [{ base: [], spaces: [], feature: {} }], + }, + ]); + + findTestSubject(wrapper, 'showReservedRolesSwitch').simulate('click'); + + expect(wrapper.find(EuiBasicTable).props().items).toEqual([ + { + name: 'disabled-role', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [{ base: [], spaces: [], feature: {} }], + transient_metadata: { enabled: false }, + }, + { + name: 'test-role-1', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [{ base: [], spaces: [], feature: {} }], + }, + ]); + }); }); diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx index 7c686bef391a7..04a74a1a9b99a 100644 --- a/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx @@ -8,7 +8,6 @@ import _ from 'lodash'; import React, { Component } from 'react'; import { EuiButton, - EuiIcon, EuiInMemoryTable, EuiLink, EuiPageContent, @@ -19,14 +18,26 @@ import { EuiTitle, EuiButtonIcon, EuiBasicTableColumn, + EuiSwitchEvent, + EuiSwitch, + EuiFlexGroup, + EuiFlexItem, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { NotificationsStart } from 'src/core/public'; -import { Role, isRoleEnabled, isReadOnlyRole, isReservedRole } from '../../../../common/model'; +import { + Role, + isRoleEnabled, + isRoleReadOnly, + isRoleReserved, + isRoleDeprecated, + getExtendedRoleDeprecationNotice, +} from '../../../../common/model'; import { RolesAPIClient } from '../roles_api_client'; import { ConfirmDelete } from './confirm_delete'; import { PermissionDenied } from './permission_denied'; +import { DisabledBadge, DeprecatedBadge, ReservedBadge } from '../../badges'; interface Props { notifications: NotificationsStart; @@ -35,10 +46,12 @@ interface Props { interface State { roles: Role[]; + visibleRoles: Role[]; selection: Role[]; filter: string; showDeleteConfirmation: boolean; permissionDenied: boolean; + includeReservedRoles: boolean; } const getRoleManagementHref = (action: 'edit' | 'clone', roleName?: string) => { @@ -50,10 +63,12 @@ export class RolesGridPage extends Component { super(props); this.state = { roles: [], + visibleRoles: [], selection: [], filter: '', showDeleteConfirmation: false, permissionDenied: false, + includeReservedRoles: true, }; } @@ -125,16 +140,22 @@ export class RolesGridPage extends Component { initialPageSize: 20, pageSizeOptions: [10, 20, 30, 50, 100], }} - items={this.getVisibleRoles()} + items={this.state.visibleRoles} loading={roles.length === 0} search={{ toolsLeft: this.renderToolsLeft(), + toolsRight: this.renderToolsRight(), box: { incremental: true, }, onChange: (query: Record) => { this.setState({ filter: query.queryText, + visibleRoles: this.getVisibleRoles( + this.state.roles, + query.queryText, + this.state.includeReservedRoles + ), }); }, }} @@ -158,11 +179,6 @@ export class RolesGridPage extends Component { }; private getColumnConfig = () => { - const reservedRoleDesc = i18n.translate( - 'xpack.security.management.roles.reservedColumnDescription', - { defaultMessage: 'Reserved roles are built-in and cannot be edited or removed.' } - ); - return [ { field: 'name', @@ -177,35 +193,18 @@ export class RolesGridPage extends Component { {name} - {!isRoleEnabled(record) && ( - - )} ); }, }, { field: 'metadata', - name: i18n.translate('xpack.security.management.roles.reservedColumnName', { - defaultMessage: 'Reserved', + name: i18n.translate('xpack.security.management.roles.statusColumnName', { + defaultMessage: 'Status', }), - sortable: ({ metadata }: Role) => Boolean(metadata && metadata._reserved), - dataType: 'boolean', - align: 'right', - description: reservedRoleDesc, - render: (metadata: Role['metadata']) => { - const label = i18n.translate('xpack.security.management.roles.reservedRoleIconLabel', { - defaultMessage: 'Reserved role', - }); - - return metadata && metadata._reserved ? ( - - - - ) : null; + sortable: (role: Role) => isRoleEnabled(role) && !isRoleDeprecated(role), + render: (metadata: Role['metadata'], record: Role) => { + return this.getRoleStatusBadges(record); }, }, { @@ -215,7 +214,7 @@ export class RolesGridPage extends Component { width: '150px', actions: [ { - available: (role: Role) => !isReadOnlyRole(role), + available: (role: Role) => !isRoleReadOnly(role), render: (role: Role) => { const title = i18n.translate('xpack.security.management.roles.editRoleActionName', { defaultMessage: `Edit {roleName}`, @@ -235,7 +234,7 @@ export class RolesGridPage extends Component { }, }, { - available: (role: Role) => !isReservedRole(role), + available: (role: Role) => !isRoleReserved(role), render: (role: Role) => { const title = i18n.translate('xpack.security.management.roles.cloneRoleActionName', { defaultMessage: `Clone {roleName}`, @@ -259,16 +258,64 @@ export class RolesGridPage extends Component { ] as Array>; }; - private getVisibleRoles = () => { - const { roles, filter } = this.state; + private getVisibleRoles = (roles: Role[], filter: string, includeReservedRoles: boolean) => { + return roles.filter(role => { + const normalized = `${role.name}`.toLowerCase(); + const normalizedQuery = filter.toLowerCase(); + return ( + normalized.indexOf(normalizedQuery) !== -1 && + (includeReservedRoles || !isRoleReserved(role)) + ); + }); + }; + + private onIncludeReservedRolesChange = (e: EuiSwitchEvent) => { + this.setState({ + includeReservedRoles: e.target.checked, + visibleRoles: this.getVisibleRoles(this.state.roles, this.state.filter, e.target.checked), + }); + }; + + private getRoleStatusBadges = (role: Role) => { + const enabled = isRoleEnabled(role); + const deprecated = isRoleDeprecated(role); + const reserved = isRoleReserved(role); - return filter - ? roles.filter(({ name }) => { - const normalized = `${name}`.toLowerCase(); - const normalizedQuery = filter.toLowerCase(); - return normalized.indexOf(normalizedQuery) !== -1; - }) - : roles; + const badges = []; + if (!enabled) { + badges.push(); + } + if (reserved) { + badges.push( + + } + /> + ); + } + if (deprecated) { + badges.push( + + ); + } + + return ( + + {badges.map((badge, index) => ( + + {badge} + + ))} + + ); }; private handleDelete = () => { @@ -283,7 +330,14 @@ export class RolesGridPage extends Component { try { const roles = await this.props.rolesAPIClient.getRoles(); - this.setState({ roles }); + this.setState({ + roles, + visibleRoles: this.getVisibleRoles( + roles, + this.state.filter, + this.state.includeReservedRoles + ), + }); } catch (e) { if (_.get(e, 'body.statusCode') === 403) { this.setState({ permissionDenied: true }); @@ -320,6 +374,21 @@ export class RolesGridPage extends Component { ); } + private renderToolsRight() { + return ( + + } + checked={this.state.includeReservedRoles} + onChange={this.onIncludeReservedRolesChange} + /> + ); + } private onCancelDelete = () => { this.setState({ showDeleteConfirmation: false }); }; diff --git a/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.test.tsx b/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.test.tsx index be46612767a59..d41a05e00e53c 100644 --- a/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.test.tsx +++ b/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.test.tsx @@ -40,7 +40,7 @@ describe('', () => { ); @@ -68,7 +68,7 @@ describe('', () => { user={user} isUserChangingOwnPassword={true} onChangePassword={callback} - apiClient={apiClientMock} + userAPIClient={apiClientMock} notifications={coreMock.createStart().notifications} /> ); @@ -107,7 +107,7 @@ describe('', () => { ); diff --git a/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.tsx b/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.tsx index 75621762b1b85..047cad7bead81 100644 --- a/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.tsx +++ b/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.tsx @@ -23,7 +23,7 @@ interface Props { user: User; isUserChangingOwnPassword: boolean; onChangePassword?: () => void; - apiClient: PublicMethodsOf; + userAPIClient: PublicMethodsOf; notifications: NotificationsStart; } @@ -279,7 +279,7 @@ export class ChangePasswordForm extends Component { private performPasswordChange = async () => { try { - await this.props.apiClient.changePassword( + await this.props.userAPIClient.changePassword( this.props.user.username, this.state.newPassword, this.state.currentPassword diff --git a/x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.test.tsx b/x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.test.tsx index bcec707b03f93..9c5a8b0b75ead 100644 --- a/x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.test.tsx +++ b/x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.test.tsx @@ -15,7 +15,7 @@ describe('ConfirmDeleteUsers', () => { it('renders a warning for a single user', () => { const wrapper = mountWithIntl( { it('renders a warning for a multiple users', () => { const wrapper = mountWithIntl( { const onCancel = jest.fn(); const wrapper = mountWithIntl( { const wrapper = mountWithIntl( @@ -90,7 +90,7 @@ describe('ConfirmDeleteUsers', () => { const wrapper = mountWithIntl( diff --git a/x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.tsx b/x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.tsx index b7269e0168d7d..53acbf42273e8 100644 --- a/x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.tsx +++ b/x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.tsx @@ -13,7 +13,7 @@ import { UserAPIClient } from '../..'; interface Props { usersToDelete: string[]; - apiClient: PublicMethodsOf; + userAPIClient: PublicMethodsOf; notifications: NotificationsStart; onCancel: () => void; callback?: (usersToDelete: string[], errors: string[]) => void; @@ -77,11 +77,11 @@ export class ConfirmDeleteUsers extends Component { } private deleteUsers = () => { - const { usersToDelete, callback, apiClient, notifications } = this.props; + const { usersToDelete, callback, userAPIClient, notifications } = this.props; const errors: string[] = []; usersToDelete.forEach(async username => { try { - await apiClient.deleteUser(username); + await userAPIClient.deleteUser(username); notifications.toasts.addSuccess( i18n.translate( 'xpack.security.management.users.confirmDelete.userSuccessfullyDeletedNotificationMessage', diff --git a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx index 7a7542909431f..be7517ff892b5 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx @@ -15,13 +15,14 @@ import { mockAuthenticatedUser } from '../../../../common/model/authenticated_us import { securityMock } from '../../../mocks'; import { rolesAPIClientMock } from '../../roles/index.mock'; import { userAPIClientMock } from '../index.mock'; +import { findTestSubject } from 'test_utils/find_test_subject'; -const createUser = (username: string) => { +const createUser = (username: string, roles = ['idk', 'something']) => { const user: User = { username, full_name: 'my full name', email: 'foo@bar.com', - roles: ['idk', 'something'], + roles, enabled: true, }; @@ -34,9 +35,9 @@ const createUser = (username: string) => { return user; }; -const buildClients = () => { +const buildClients = (user: User) => { const apiClient = userAPIClientMock.create(); - apiClient.getUser.mockImplementation(async (username: string) => createUser(username)); + apiClient.getUser.mockResolvedValue(user); const rolesAPIClient = rolesAPIClientMock.create(); rolesAPIClient.getRoles.mockImplementation(() => { @@ -59,6 +60,18 @@ const buildClients = () => { }, kibana: [], }, + { + name: 'deprecated-role', + elasticsearch: { + cluster: [], + indices: [], + run_as: ['bar'], + }, + kibana: [], + metadata: { + _deprecated: true, + }, + }, ] as Role[]); }); @@ -83,12 +96,13 @@ function expectMissingSaveButton(wrapper: ReactWrapper) { describe('EditUserPage', () => { it('allows reserved users to be viewed', async () => { - const { apiClient, rolesAPIClient } = buildClients(); + const user = createUser('reserved_user'); + const { apiClient, rolesAPIClient } = buildClients(user); const securitySetup = buildSecuritySetup(); const wrapper = mountWithIntl( { }); it('allows new users to be created', async () => { - const { apiClient, rolesAPIClient } = buildClients(); + const user = createUser(''); + const { apiClient, rolesAPIClient } = buildClients(user); const securitySetup = buildSecuritySetup(); const wrapper = mountWithIntl( { }); it('allows existing users to be edited', async () => { - const { apiClient, rolesAPIClient } = buildClients(); + const user = createUser('existing_user'); + const { apiClient, rolesAPIClient } = buildClients(user); const securitySetup = buildSecuritySetup(); const wrapper = mountWithIntl( { expect(apiClient.getUser).toBeCalledTimes(1); expect(securitySetup.authc.getCurrentUser).toBeCalledTimes(1); + expect(findTestSubject(wrapper, 'hasDeprecatedRolesAssignedHelpText')).toHaveLength(0); expectSaveButton(wrapper); }); + + it('warns when user is assigned a deprecated role', async () => { + const user = createUser('existing_user', ['deprecated-role']); + const { apiClient, rolesAPIClient } = buildClients(user); + const securitySetup = buildSecuritySetup(); + + const wrapper = mountWithIntl( + + ); + + await waitForRender(wrapper); + + expect(apiClient.getUser).toBeCalledTimes(1); + expect(securitySetup.authc.getCurrentUser).toBeCalledTimes(1); + + expect(findTestSubject(wrapper, 'hasDeprecatedRolesAssignedHelpText')).toHaveLength(1); + }); }); async function waitForRender(wrapper: ReactWrapper) { diff --git a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx index 8e7d9fb2dac08..6417ce81b647d 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx @@ -18,7 +18,6 @@ import { EuiIcon, EuiText, EuiFieldText, - EuiComboBox, EuiPageContent, EuiPageContentHeader, EuiPageContentHeaderSection, @@ -29,17 +28,18 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { NotificationsStart } from 'src/core/public'; -import { User, EditUser, Role } from '../../../../common/model'; +import { User, EditUser, Role, isRoleDeprecated } from '../../../../common/model'; import { AuthenticationServiceSetup } from '../../../authentication'; import { USERS_PATH } from '../../management_urls'; import { RolesAPIClient } from '../../roles'; import { ConfirmDeleteUsers, ChangePasswordForm } from '../components'; import { UserValidator, UserValidationResult } from './validate_user'; +import { RoleComboBox } from '../../role_combo_box'; import { UserAPIClient } from '..'; interface Props { username?: string; - apiClient: PublicMethodsOf; + userAPIClient: PublicMethodsOf; rolesAPIClient: PublicMethodsOf; authc: AuthenticationServiceSetup; notifications: NotificationsStart; @@ -53,7 +53,7 @@ interface State { showDeleteConfirmation: boolean; user: EditUser; roles: Role[]; - selectedRoles: Array<{ label: string }>; + selectedRoles: string[]; formError: UserValidationResult | null; } @@ -99,12 +99,12 @@ export class EditUserPage extends Component { } private async setCurrentUser() { - const { username, apiClient, rolesAPIClient, notifications, authc } = this.props; + const { username, userAPIClient, rolesAPIClient, notifications, authc } = this.props; let { user, currentUser } = this.state; if (username) { try { user = { - ...(await apiClient.getUser(username)), + ...(await userAPIClient.getUser(username)), password: '', confirmPassword: '', }; @@ -138,7 +138,7 @@ export class EditUserPage extends Component { currentUser, user, roles, - selectedRoles: user.roles.map(role => ({ label: role })) || [], + selectedRoles: user.roles || [], }); } @@ -160,18 +160,16 @@ export class EditUserPage extends Component { this.setState({ formError: null, }); - const { apiClient } = this.props; + const { userAPIClient } = this.props; const { user, isNewUser, selectedRoles } = this.state; const userToSave: EditUser = { ...user }; if (!isNewUser) { delete userToSave.password; } delete userToSave.confirmPassword; - userToSave.roles = selectedRoles.map(selectedRole => { - return selectedRole.label; - }); + userToSave.roles = [...selectedRoles]; try { - await apiClient.saveUser(userToSave); + await userAPIClient.saveUser(userToSave); this.props.notifications.toasts.addSuccess( i18n.translate( 'xpack.security.management.users.editUser.userSuccessfullySavedNotificationMessage', @@ -269,7 +267,7 @@ export class EditUserPage extends Component { user={this.state.user} isUserChangingOwnPassword={userIsLoggedInUser} onChangePassword={this.toggleChangePasswordForm} - apiClient={this.props.apiClient} + userAPIClient={this.props.userAPIClient} notifications={this.props.notifications} /> @@ -346,7 +344,7 @@ export class EditUserPage extends Component { }); }; - private onRolesChange = (selectedRoles: Array<{ label: string }>) => { + private onRolesChange = (selectedRoles: string[]) => { this.setState({ selectedRoles, }); @@ -365,8 +363,8 @@ export class EditUserPage extends Component { public render() { const { user, - roles, selectedRoles, + roles, showChangePasswordForm, isNewUser, showDeleteConfirmation, @@ -380,6 +378,22 @@ export class EditUserPage extends Component { return null; } + const hasAnyDeprecatedRolesAssigned = selectedRoles.some(selected => { + const role = roles.find(r => r.name === selected); + return role && isRoleDeprecated(role); + }); + + const roleHelpText = hasAnyDeprecatedRolesAssigned ? ( + + + + ) : ( + undefined + ); + return (
@@ -426,7 +440,7 @@ export class EditUserPage extends Component { onCancel={this.onCancelDelete} usersToDelete={[user.username]} callback={this.handleDelete} - apiClient={this.props.apiClient} + userAPIClient={this.props.userAPIClient} notifications={this.props.notifications} /> ) : null} @@ -492,19 +506,13 @@ export class EditUserPage extends Component { 'xpack.security.management.users.editUser.rolesFormRowLabel', { defaultMessage: 'Roles' } )} + helpText={roleHelpText} > - { - return { 'data-test-subj': `roleOption-${role.name}`, label: role.name }; - })} - selectedOptions={selectedRoles} /> diff --git a/x-pack/plugins/security/public/management/users/user_utils.ts b/x-pack/plugins/security/public/management/users/user_utils.ts new file mode 100644 index 0000000000000..f46f6f897e23b --- /dev/null +++ b/x-pack/plugins/security/public/management/users/user_utils.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { User } from '../../../common/model'; + +export const isUserReserved = (user: User) => user.metadata?._reserved ?? false; diff --git a/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.test.tsx b/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.test.tsx index def0649953437..031b67d5d9122 100644 --- a/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.test.tsx +++ b/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.test.tsx @@ -5,12 +5,15 @@ */ import { User } from '../../../../common/model'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; import { UsersGridPage } from './users_grid_page'; import React from 'react'; import { ReactWrapper } from 'enzyme'; import { userAPIClientMock } from '../index.mock'; import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { rolesAPIClientMock } from '../../roles/index.mock'; +import { findTestSubject } from 'test_utils/find_test_subject'; +import { EuiBasicTable } from '@elastic/eui'; describe('UsersGridPage', () => { it('renders the list of users', async () => { @@ -39,7 +42,8 @@ describe('UsersGridPage', () => { const wrapper = mountWithIntl( ); @@ -49,6 +53,7 @@ describe('UsersGridPage', () => { expect(apiClientMock.getUsers).toBeCalledTimes(1); expect(wrapper.find('EuiInMemoryTable')).toHaveLength(1); expect(wrapper.find('EuiTableRow')).toHaveLength(2); + expect(findTestSubject(wrapper, 'userDisabled')).toHaveLength(0); }); it('renders a forbidden message if user is not authorized', async () => { @@ -56,7 +61,11 @@ describe('UsersGridPage', () => { apiClient.getUsers.mockRejectedValue({ body: { statusCode: 403 } }); const wrapper = mountWithIntl( - + ); await waitForRender(wrapper); @@ -65,10 +74,172 @@ describe('UsersGridPage', () => { expect(wrapper.find('[data-test-subj="permissionDeniedMessage"]')).toHaveLength(1); expect(wrapper.find('EuiInMemoryTable')).toHaveLength(0); }); + + it('renders disabled users', async () => { + const apiClientMock = userAPIClientMock.create(); + apiClientMock.getUsers.mockImplementation(() => { + return Promise.resolve([ + { + username: 'foo', + email: 'foo@bar.net', + full_name: 'foo bar', + roles: ['kibana_user'], + enabled: false, + }, + ]); + }); + + const wrapper = mountWithIntl( + + ); + + await waitForRender(wrapper); + + expect(findTestSubject(wrapper, 'userDisabled')).toHaveLength(1); + }); + + it('renders a warning when a user is assigned a deprecated role', async () => { + const apiClientMock = userAPIClientMock.create(); + apiClientMock.getUsers.mockImplementation(() => { + return Promise.resolve([ + { + username: 'foo', + email: 'foo@bar.net', + full_name: 'foo bar', + roles: ['kibana_user'], + enabled: true, + }, + { + username: 'reserved', + email: 'reserved@bar.net', + full_name: '', + roles: ['superuser'], + enabled: true, + metadata: { + _reserved: true, + }, + }, + ]); + }); + + const roleAPIClientMock = rolesAPIClientMock.create(); + roleAPIClientMock.getRoles.mockResolvedValue([ + { + name: 'kibana_user', + metadata: { + _deprecated: true, + _deprecated_reason: `I don't like you.`, + }, + }, + ]); + + const wrapper = mountWithIntl( + + ); + + await waitForRender(wrapper); + + const deprecationTooltip = wrapper.find('[data-test-subj="roleDeprecationTooltip"]').props(); + + expect(deprecationTooltip).toMatchInlineSnapshot(` + Object { + "children":
+ kibana_user + + +
, + "content": "The kibana_user role is deprecated. I don't like you.", + "data-test-subj": "roleDeprecationTooltip", + "delay": "regular", + "position": "top", + } + `); + }); + + it('hides reserved users when instructed to', async () => { + const apiClientMock = userAPIClientMock.create(); + apiClientMock.getUsers.mockImplementation(() => { + return Promise.resolve([ + { + username: 'foo', + email: 'foo@bar.net', + full_name: 'foo bar', + roles: ['kibana_user'], + enabled: true, + }, + { + username: 'reserved', + email: 'reserved@bar.net', + full_name: '', + roles: ['superuser'], + enabled: true, + metadata: { + _reserved: true, + }, + }, + ]); + }); + + const roleAPIClientMock = rolesAPIClientMock.create(); + + const wrapper = mountWithIntl( + + ); + + await waitForRender(wrapper); + + expect(wrapper.find(EuiBasicTable).props().items).toEqual([ + { + username: 'foo', + email: 'foo@bar.net', + full_name: 'foo bar', + roles: ['kibana_user'], + enabled: true, + }, + { + username: 'reserved', + email: 'reserved@bar.net', + full_name: '', + roles: ['superuser'], + enabled: true, + metadata: { + _reserved: true, + }, + }, + ]); + + findTestSubject(wrapper, 'showReservedUsersSwitch').simulate('click'); + + expect(wrapper.find(EuiBasicTable).props().items).toEqual([ + { + username: 'foo', + email: 'foo@bar.net', + full_name: 'foo bar', + roles: ['kibana_user'], + enabled: true, + }, + ]); + }); }); async function waitForRender(wrapper: ReactWrapper) { - await Promise.resolve(); - await Promise.resolve(); + await nextTick(); wrapper.update(); } diff --git a/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx b/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx index fa15c3388fcc9..6837fcf430fe7 100644 --- a/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx +++ b/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx @@ -4,10 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Component, Fragment } from 'react'; +import React, { Component } from 'react'; import { EuiButton, - EuiIcon, EuiLink, EuiFlexGroup, EuiInMemoryTable, @@ -18,25 +17,36 @@ import { EuiPageContentBody, EuiEmptyPrompt, EuiBasicTableColumn, + EuiSwitchEvent, + EuiSwitch, + EuiFlexItem, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { NotificationsStart } from 'src/core/public'; -import { User } from '../../../../common/model'; +import { User, Role } from '../../../../common/model'; import { ConfirmDeleteUsers } from '../components'; +import { isUserReserved } from '../user_utils'; +import { DisabledBadge, ReservedBadge } from '../../badges'; +import { RoleTableDisplay } from '../../role_table_display'; +import { RolesAPIClient } from '../../roles'; import { UserAPIClient } from '..'; interface Props { - apiClient: PublicMethodsOf; + userAPIClient: PublicMethodsOf; + rolesAPIClient: PublicMethodsOf; notifications: NotificationsStart; } interface State { users: User[]; + visibleUsers: User[]; + roles: null | Role[]; selection: User[]; showDeleteConfirmation: boolean; permissionDenied: boolean; filter: string; + includeReservedUsers: boolean; } export class UsersGridPage extends Component { @@ -44,19 +54,22 @@ export class UsersGridPage extends Component { super(props); this.state = { users: [], + visibleUsers: [], + roles: [], selection: [], showDeleteConfirmation: false, permissionDenied: false, filter: '', + includeReservedUsers: true, }; } public componentDidMount() { - this.loadUsers(); + this.loadUsersAndRoles(); } public render() { - const { users, filter, permissionDenied, showDeleteConfirmation, selection } = this.state; + const { users, roles, permissionDenied, showDeleteConfirmation, selection } = this.state; if (permissionDenied) { return ( @@ -86,17 +99,6 @@ export class UsersGridPage extends Component { } const path = '#/management/security/'; const columns: Array> = [ - { - field: 'full_name', - name: i18n.translate('xpack.security.management.users.fullNameColumnName', { - defaultMessage: 'Full Name', - }), - sortable: true, - truncateText: true, - render: (fullName: string) => { - return
{fullName}
; - }, - }, { field: 'username', name: i18n.translate('xpack.security.management.users.userNameColumnName', { @@ -110,6 +112,18 @@ export class UsersGridPage extends Component { ), }, + { + field: 'full_name', + name: i18n.translate('xpack.security.management.users.fullNameColumnName', { + defaultMessage: 'Full Name', + }), + sortable: true, + truncateText: true, + render: (fullName: string) => { + return
{fullName}
; + }, + }, + { field: 'email', name: i18n.translate('xpack.security.management.users.emailAddressColumnName', { @@ -126,34 +140,27 @@ export class UsersGridPage extends Component { name: i18n.translate('xpack.security.management.users.rolesColumnName', { defaultMessage: 'Roles', }), + width: '30%', render: (rolenames: string[]) => { const roleLinks = rolenames.map((rolename, index) => { - return ( - - {rolename} - {index === rolenames.length - 1 ? null : ', '} - - ); + const roleDefinition = roles?.find(role => role.name === rolename) ?? rolename; + return ; }); return
{roleLinks}
; }, }, { field: 'metadata', - name: i18n.translate('xpack.security.management.users.reservedColumnName', { - defaultMessage: 'Reserved', + name: i18n.translate('xpack.security.management.users.statusColumnName', { + defaultMessage: 'Status', }), + width: '10%', sortable: ({ metadata }: User) => Boolean(metadata && metadata._reserved), - width: '100px', - align: 'right', description: i18n.translate('xpack.security.management.users.reservedColumnDescription', { defaultMessage: 'Reserved users are built-in and cannot be removed. Only the password can be changed.', }), - render: (metadata: User['metadata']) => - metadata && metadata._reserved ? ( - - ) : null, + render: (metadata: User['metadata'], record: User) => this.getUserStatusBadges(record), }, ]; const pagination = { @@ -170,18 +177,24 @@ export class UsersGridPage extends Component { }; const search = { toolsLeft: this.renderToolsLeft(), + toolsRight: this.renderToolsRight(), box: { incremental: true, }, onChange: (query: any) => { this.setState({ filter: query.queryText, + visibleUsers: this.getVisibleUsers( + this.state.users, + query.queryText, + this.state.includeReservedUsers + ), }); }, }; const sorting = { sort: { - field: 'full_name', + field: 'username', direction: 'asc', }, } as const; @@ -190,13 +203,7 @@ export class UsersGridPage extends Component { 'data-test-subj': 'userRow', }; }; - const usersToShow = filter - ? users.filter(({ username, roles, full_name: fullName = '', email = '' }) => { - const normalized = `${username} ${roles.join(' ')} ${fullName} ${email}`.toLowerCase(); - const normalizedQuery = filter.toLowerCase(); - return normalized.indexOf(normalizedQuery) !== -1; - }) - : users; + return (
@@ -226,7 +233,7 @@ export class UsersGridPage extends Component { onCancel={this.onCancelDelete} usersToDelete={selection.map(user => user.username)} callback={this.handleDelete} - apiClient={this.props.apiClient} + userAPIClient={this.props.userAPIClient} notifications={this.props.notifications} /> ) : null} @@ -237,7 +244,7 @@ export class UsersGridPage extends Component { columns={columns} selection={selectionConfig} pagination={pagination} - items={usersToShow} + items={this.state.visibleUsers} loading={users.length === 0} search={search} sorting={sorting} @@ -262,10 +269,34 @@ export class UsersGridPage extends Component { }); }; - private async loadUsers() { + private getVisibleUsers = (users: User[], filter: string, includeReservedUsers: boolean) => { + return users.filter( + ({ username, roles: userRoles, full_name: fullName = '', email = '', metadata = {} }) => { + const normalized = `${username} ${userRoles.join(' ')} ${fullName} ${email}`.toLowerCase(); + const normalizedQuery = filter.toLowerCase(); + return ( + normalized.indexOf(normalizedQuery) !== -1 && + (includeReservedUsers || !metadata._reserved) + ); + } + ); + }; + + private async loadUsersAndRoles() { try { - const users = await this.props.apiClient.getUsers(); - this.setState({ users }); + const [users, roles] = await Promise.all([ + this.props.userAPIClient.getUsers(), + this.props.rolesAPIClient.getRoles(), + ]); + this.setState({ + users, + roles, + visibleUsers: this.getVisibleUsers( + users, + this.state.filter, + this.state.includeReservedUsers + ), + }); } catch (e) { if (e.body.statusCode === 403) { this.setState({ permissionDenied: true }); @@ -303,6 +334,62 @@ export class UsersGridPage extends Component { ); } + private onIncludeReservedUsersChange = (e: EuiSwitchEvent) => { + this.setState({ + includeReservedUsers: e.target.checked, + visibleUsers: this.getVisibleUsers(this.state.users, this.state.filter, e.target.checked), + }); + }; + + private renderToolsRight() { + return ( + + } + checked={this.state.includeReservedUsers} + onChange={this.onIncludeReservedUsersChange} + /> + ); + } + + private getUserStatusBadges = (user: User) => { + const enabled = user.enabled; + const reserved = isUserReserved(user); + + const badges = []; + if (!enabled) { + badges.push(); + } + if (reserved) { + badges.push( + + } + /> + ); + } + + return ( + + {badges.map((badge, index) => ( + + {badge} + + ))} + + ); + }; + private onCancelDelete = () => { this.setState({ showDeleteConfirmation: false }); }; diff --git a/x-pack/plugins/security/public/management/users/users_management_app.test.tsx b/x-pack/plugins/security/public/management/users/users_management_app.test.tsx index fd81756f176f7..05491d6f889b6 100644 --- a/x-pack/plugins/security/public/management/users/users_management_app.test.tsx +++ b/x-pack/plugins/security/public/management/users/users_management_app.test.tsx @@ -58,7 +58,7 @@ describe('usersManagementApp', () => { expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: `#${basePath}`, text: 'Users' }]); expect(container).toMatchInlineSnapshot(`
- Users Page: {"notifications":{"toasts":{}},"apiClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}}} + Users Page: {"notifications":{"toasts":{}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}}}
`); @@ -80,7 +80,7 @@ describe('usersManagementApp', () => { ]); expect(container).toMatchInlineSnapshot(`
- User Edit Page: {"authc":{},"apiClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"notifications":{"toasts":{}}} + User Edit Page: {"authc":{},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"notifications":{"toasts":{}}}
`); @@ -103,7 +103,7 @@ describe('usersManagementApp', () => { ]); expect(container).toMatchInlineSnapshot(`
- User Edit Page: {"authc":{},"apiClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"notifications":{"toasts":{}},"username":"someUserName"} + User Edit Page: {"authc":{},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{}}},"notifications":{"toasts":{}},"username":"someUserName"}
`); diff --git a/x-pack/plugins/security/public/management/users/users_management_app.tsx b/x-pack/plugins/security/public/management/users/users_management_app.tsx index 9aebb396ce9a9..7874b810676b5 100644 --- a/x-pack/plugins/security/public/management/users/users_management_app.tsx +++ b/x-pack/plugins/security/public/management/users/users_management_app.tsx @@ -39,9 +39,16 @@ export const usersManagementApp = Object.freeze({ ]; const userAPIClient = new UserAPIClient(http); + const rolesAPIClient = new RolesAPIClient(http); const UsersGridPageWithBreadcrumbs = () => { setBreadcrumbs(usersBreadcrumbs); - return ; + return ( + + ); }; const EditUserPageWithBreadcrumbs = () => { @@ -61,7 +68,7 @@ export const usersManagementApp = Object.freeze({ return ( ), diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts index 9cbb2c30e4baf..40e35085ea18a 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts @@ -41,12 +41,19 @@ describe.skip('onPostAuthInterceptor', () => { ).toString('base64')}`, }; + /** + * + * commented out due to hooks being called regardless of skip + * https://github.com/facebook/jest/issues/8379 + beforeEach(async () => { root = kbnTestServer.createRoot(); }); afterEach(async () => await root.shutdown()); + */ + function initKbnServer(router: IRouter, basePath: IBasePath, routes: 'legacy' | 'new-platform') { const kbnServer = kbnTestServer.getKbnServer(root); diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts index 1b673d3418983..1558c6425f542 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts @@ -22,12 +22,19 @@ import { elasticsearchServiceMock } from 'src/core/server/mocks'; describe.skip('onRequestInterceptor', () => { let root: ReturnType; + /** + * + * commented out due to hooks being called regardless of skip + * https://github.com/facebook/jest/issues/8379 + beforeEach(async () => { root = kbnTestServer.createRoot(); }, 30000); afterEach(async () => await root.shutdown()); + */ + function initKbnServer(router: IRouter, basePath: IBasePath, routes: 'legacy' | 'new-platform') { const kbnServer = kbnTestServer.getKbnServer(root); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 0504343e4dcc3..0e2a842db19c8 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -7537,9 +7537,6 @@ "xpack.ml.calendarsList.table.idColumnName": "ID", "xpack.ml.calendarsList.table.jobsColumnName": "ジョブ", "xpack.ml.calendarsList.table.newButtonLabel": "新規", - "xpack.ml.checkLicense.licenseHasExpiredMessage": "{licenseTypeName} 機械学習ライセンスが期限切れになりました。", - "xpack.ml.checkLicense.licenseInformationNotAvailableThisTimeMessage": "現在ライセンス情報が利用できないため機械学習を使用できません。", - "xpack.ml.checkLicense.mlIsUnavailableMessage": "機械学習が利用できません", "xpack.ml.controls.checkboxShowCharts.showChartsCheckboxLabel": "チャートを表示", "xpack.ml.controls.selectInterval.autoLabel": "自動", "xpack.ml.controls.selectInterval.dayLabel": "1 日", @@ -10662,14 +10659,10 @@ "xpack.security.management.roles.deleteSelectedRolesButtonLabel": "ロール {numSelected} {numSelected, plural, one { } other {}} を削除しました", "xpack.security.management.roles.deletingRolesWarningMessage": "この操作は元に戻すことができません。", "xpack.security.management.roles.deniedPermissionTitle": "ロールを管理するにはパーミッションが必要です", - "xpack.security.management.roles.disabledTooltip": " (無効)", "xpack.security.management.roles.editRoleActionName": "{roleName} を編集", "xpack.security.management.roles.fetchingRolesErrorMessage": "ロールの取得中にエラーが発生: {message}", "xpack.security.management.roles.nameColumnName": "ロール", "xpack.security.management.roles.noPermissionToManageRolesDescription": "システム管理者にお問い合わせください。", - "xpack.security.management.roles.reservedColumnDescription": "リザーブされたロールはビルトインのため削除または変更できません。", - "xpack.security.management.roles.reservedColumnName": "リザーブ", - "xpack.security.management.roles.reservedRoleIconLabel": "指定済みロール", "xpack.security.management.roles.roleNotFound": "「{roleName}」ロールが見つかりません。", "xpack.security.management.roles.roleTitle": "ロール", "xpack.security.management.roles.subtitle": "ユーザーのグループにロールを適用してスタック全体のパーミッションを管理", @@ -10720,7 +10713,6 @@ "xpack.security.management.users.fullNameColumnName": "フルネーム", "xpack.security.management.users.permissionDeniedToManageUsersDescription": "システム管理者にお問い合わせください。", "xpack.security.management.users.reservedColumnDescription": "リザーブされたユーザーはビルトインのため削除できません。パスワードのみ変更できます。", - "xpack.security.management.users.reservedColumnName": "リザーブ", "xpack.security.management.users.rolesColumnName": "ロール", "xpack.security.management.users.userNameColumnName": "ユーザー名", "xpack.security.management.users.usersTitle": "ユーザー", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 156b1d3d24153..95bc32a9a1c52 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -7537,9 +7537,6 @@ "xpack.ml.calendarsList.table.idColumnName": "ID", "xpack.ml.calendarsList.table.jobsColumnName": "作业", "xpack.ml.calendarsList.table.newButtonLabel": "新建", - "xpack.ml.checkLicense.licenseHasExpiredMessage": "您的 {licenseTypeName} Machine Learning 许可证已过期。", - "xpack.ml.checkLicense.licenseInformationNotAvailableThisTimeMessage": "您不能使用 Machine Learning,因为许可证信息当前不可用。", - "xpack.ml.checkLicense.mlIsUnavailableMessage": "Machine Learning 不可用", "xpack.ml.controls.checkboxShowCharts.showChartsCheckboxLabel": "显示图表", "xpack.ml.controls.selectInterval.autoLabel": "自动", "xpack.ml.controls.selectInterval.dayLabel": "1 天", @@ -10662,14 +10659,10 @@ "xpack.security.management.roles.deleteSelectedRolesButtonLabel": "删除 {numSelected} 个角色{numSelected, plural, one {} other {}}", "xpack.security.management.roles.deletingRolesWarningMessage": "此操作无法撤消。", "xpack.security.management.roles.deniedPermissionTitle": "您需要用于管理角色的权限", - "xpack.security.management.roles.disabledTooltip": " (已禁用)", "xpack.security.management.roles.editRoleActionName": "编辑 {roleName}", "xpack.security.management.roles.fetchingRolesErrorMessage": "获取用户时出错:{message}", "xpack.security.management.roles.nameColumnName": "角色", "xpack.security.management.roles.noPermissionToManageRolesDescription": "请联系您的管理员。", - "xpack.security.management.roles.reservedColumnDescription": "保留角色为内置角色,不能编辑或移除。", - "xpack.security.management.roles.reservedColumnName": "保留", - "xpack.security.management.roles.reservedRoleIconLabel": "保留角色", "xpack.security.management.roles.roleNotFound": "未找到任何“{roleName}”。", "xpack.security.management.roles.roleTitle": "角色", "xpack.security.management.roles.subtitle": "将角色应用到用户组并管理整个堆栈的权限。", @@ -10720,7 +10713,6 @@ "xpack.security.management.users.fullNameColumnName": "全名", "xpack.security.management.users.permissionDeniedToManageUsersDescription": "请联系您的管理员。", "xpack.security.management.users.reservedColumnDescription": "保留的用户是内置的,无法删除。只能更改密码。", - "xpack.security.management.users.reservedColumnName": "保留", "xpack.security.management.users.rolesColumnName": "角色", "xpack.security.management.users.userNameColumnName": "用户名", "xpack.security.management.users.usersTitle": "用户", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_add.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_add.test.tsx index 05adccf982b7f..cc3b15d374718 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_add.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_add/alert_add.test.tsx @@ -19,11 +19,12 @@ import { ReactWrapper } from 'enzyme'; const actionTypeRegistry = actionTypeRegistryMock.create(); const alertTypeRegistry = alertTypeRegistryMock.create(); -describe('alert_add', () => { +// FLAKY: https://github.com/elastic/kibana/issues/58970 +describe.skip('alert_add', () => { let deps: AppDeps | null; let wrapper: ReactWrapper; - beforeAll(async () => { + async function setup() { const mockes = coreMock.createSetup(); const [ { @@ -108,9 +109,10 @@ describe('alert_add', () => { } }); await waitForRender(wrapper); - }); + } - it('renders alert add flyout', () => { + it('renders alert add flyout', async () => { + await setup(); expect(wrapper.find('[data-test-subj="addAlertFlyoutTitle"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="saveAlertButton"]').exists()).toBeTruthy(); }); diff --git a/x-pack/test/functional/apps/maps/index.js b/x-pack/test/functional/apps/maps/index.js index e8a9d7ba54bc5..44a7c4c9a5f86 100644 --- a/x-pack/test/functional/apps/maps/index.js +++ b/x-pack/test/functional/apps/maps/index.js @@ -45,6 +45,7 @@ export default function({ loadTestFile, getService }) { loadTestFile(require.resolve('./import_geojson')); loadTestFile(require.resolve('./layer_errors')); loadTestFile(require.resolve('./embeddable')); + loadTestFile(require.resolve('./visualize_create_menu')); loadTestFile(require.resolve('./discover')); }); }); diff --git a/x-pack/test/functional/apps/maps/visualize_create_menu.js b/x-pack/test/functional/apps/maps/visualize_create_menu.js new file mode 100644 index 0000000000000..ed0c153b9f99d --- /dev/null +++ b/x-pack/test/functional/apps/maps/visualize_create_menu.js @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +export default function({ getPageObjects }) { + const PageObjects = getPageObjects(['visualize', 'header', 'maps']); + + describe('visualize create menu', () => { + before(async () => { + await PageObjects.visualize.navigateToNewVisualization(); + }); + + it('should show maps application in create menu', async () => { + const hasMapsApp = await PageObjects.visualize.hasMapsApp(); + expect(hasMapsApp).to.equal(true); + }); + + it('should not show legacy region map visualizion in create menu', async () => { + const hasLegecyViz = await PageObjects.visualize.hasRegionMap(); + expect(hasLegecyViz).to.equal(false); + }); + + it('should not show legacy tilemap map visualizion in create menu', async () => { + const hasLegecyViz = await PageObjects.visualize.hasTileMap(); + expect(hasLegecyViz).to.equal(false); + }); + + it('should take users to Maps application when Maps is clicked', async () => { + await PageObjects.visualize.clickMapsApp(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.maps.waitForLayersToLoad(); + const doesLayerExist = await PageObjects.maps.doesLayerExist('Road map'); + expect(doesLayerExist).to.equal(true); + }); + }); +} diff --git a/x-pack/test/functional/apps/security/role_mappings.ts b/x-pack/test/functional/apps/security/role_mappings.ts index a1517e1934a28..827466c660015 100644 --- a/x-pack/test/functional/apps/security/role_mappings.ts +++ b/x-pack/test/functional/apps/security/role_mappings.ts @@ -28,7 +28,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('allows a role mapping to be created', async () => { await testSubjects.click('createRoleMappingButton'); await testSubjects.setValue('roleMappingFormNameInput', 'new_role_mapping'); - await testSubjects.setValue('roleMappingFormRoleComboBox', 'superuser'); + await testSubjects.setValue('rolesDropdown', 'superuser'); await browser.pressKeys(browser.keys.ENTER); await testSubjects.click('roleMappingsAddRuleButton'); diff --git a/x-pack/test/functional/apps/security/users.js b/x-pack/test/functional/apps/security/users.js index 9dc42553f0fdf..f49a74a661a63 100644 --- a/x-pack/test/functional/apps/security/users.js +++ b/x-pack/test/functional/apps/security/users.js @@ -82,13 +82,34 @@ export default function({ getService, getPageObjects }) { log.debug('actualRoles = %j', roles); // This only contains the first page of alphabetically sorted results, so the assertions are only for the first handful of expected roles. expect(roles.apm_system.reserved).to.be(true); + expect(roles.apm_system.deprecated).to.be(false); + expect(roles.apm_user.reserved).to.be(true); + expect(roles.apm_user.deprecated).to.be(false); + expect(roles.beats_admin.reserved).to.be(true); + expect(roles.beats_admin.deprecated).to.be(false); + expect(roles.beats_system.reserved).to.be(true); + expect(roles.beats_system.deprecated).to.be(false); + expect(roles.kibana_admin.reserved).to.be(true); + expect(roles.kibana_admin.deprecated).to.be(false); + + expect(roles.kibana_user.reserved).to.be(true); + expect(roles.kibana_user.deprecated).to.be(true); + + expect(roles.kibana_dashboard_only_user.reserved).to.be(true); + expect(roles.kibana_dashboard_only_user.deprecated).to.be(true); + expect(roles.kibana_system.reserved).to.be(true); + expect(roles.kibana_system.deprecated).to.be(false); + expect(roles.logstash_system.reserved).to.be(true); + expect(roles.logstash_system.deprecated).to.be(false); + expect(roles.monitoring_user.reserved).to.be(true); + expect(roles.monitoring_user.deprecated).to.be(false); }); }); } diff --git a/x-pack/test/functional/page_objects/security_page.js b/x-pack/test/functional/page_objects/security_page.js index 5889a374e443e..4803596b973bc 100644 --- a/x-pack/test/functional/page_objects/security_page.js +++ b/x-pack/test/functional/page_objects/security_page.js @@ -232,16 +232,16 @@ export function SecurityPageProvider({ getService, getPageObjects }) { const usernameElement = await user.findByCssSelector('[data-test-subj="userRowUserName"]'); const emailElement = await user.findByCssSelector('[data-test-subj="userRowEmail"]'); const rolesElement = await user.findByCssSelector('[data-test-subj="userRowRoles"]'); - const isReservedElementVisible = await user.findByCssSelector('td:last-child'); + // findAllByCssSelector is substantially faster than `find.descendantExistsByCssSelector for negative cases + const isUserReserved = + (await user.findAllByCssSelector('span[data-test-subj="userReserved"]', 1)).length > 0; return { username: await usernameElement.getVisibleText(), fullname: await fullnameElement.getVisibleText(), email: await emailElement.getVisibleText(), - roles: (await rolesElement.getVisibleText()).split(',').map(role => role.trim()), - reserved: (await isReservedElementVisible.getAttribute('innerHTML')).includes( - 'reservedUser' - ), + roles: (await rolesElement.getVisibleText()).split('\n').map(role => role.trim()), + reserved: isUserReserved, }; }); } @@ -249,15 +249,22 @@ export function SecurityPageProvider({ getService, getPageObjects }) { async getElasticsearchRoles() { const users = await testSubjects.findAll('roleRow'); return mapAsync(users, async role => { - const rolenameElement = await role.findByCssSelector('[data-test-subj="roleRowName"]'); - const reservedRoleRow = await role.findByCssSelector('td:nth-last-child(2)'); + const [rolename, reserved, deprecated] = await Promise.all([ + role.findByCssSelector('[data-test-subj="roleRowName"]').then(el => el.getVisibleText()), + // findAllByCssSelector is substantially faster than `find.descendantExistsByCssSelector for negative cases + role + .findAllByCssSelector('span[data-test-subj="roleReserved"]', 1) + .then(el => el.length > 0), + // findAllByCssSelector is substantially faster than `find.descendantExistsByCssSelector for negative cases + role + .findAllByCssSelector('span[data-test-subj="roleDeprecated"]', 1) + .then(el => el.length > 0), + ]); return { - rolename: await rolenameElement.getVisibleText(), - reserved: await find.descendantExistsByCssSelector( - '[data-test-subj="reservedRole"]', - reservedRoleRow - ), + rolename, + reserved, + deprecated, }; }); } @@ -400,7 +407,7 @@ export function SecurityPageProvider({ getService, getPageObjects }) { } async selectRole(role) { - const dropdown = await testSubjects.find('userFormRolesDropdown'); + const dropdown = await testSubjects.find('rolesDropdown'); const input = await dropdown.findByCssSelector('input'); await input.type(role); await testSubjects.click(`roleOption-${role}`); diff --git a/x-pack/test_utils/jest/contract_tests/example.contract.test.ts b/x-pack/test_utils/jest/contract_tests/example.contract.test.ts index aa452e5f1aee1..642a4f2d01644 100644 --- a/x-pack/test_utils/jest/contract_tests/example.contract.test.ts +++ b/x-pack/test_utils/jest/contract_tests/example.contract.test.ts @@ -6,19 +6,25 @@ import Slapshot from '@mattapperson/slapshot'; -import { createKibanaServer } from './servers'; -import { getEsArchiver } from './services/es_archiver'; -import { EsArchiver } from 'src/es_archiver'; -import * as path from 'path'; +// import { createKibanaServer } from './servers'; +// import { getEsArchiver } from './services/es_archiver'; +// import { EsArchiver } from 'src/es_archiver'; +// import * as path from 'path'; import * as legacyElasticsearch from 'elasticsearch'; -const { callWhenOnline, memorize } = Slapshot; +// const { callWhenOnline, memorize } = Slapshot; +const { memorize } = Slapshot; let servers: { kbnServer: any; shutdown: () => void }; -let esArchiver: EsArchiver; +// let esArchiver: EsArchiver; // FLAKY: https://github.com/elastic/kibana/issues/44250 describe.skip('Example contract tests', () => { + /** + * + * commented out due to hooks being called regardless of skip + * https://github.com/facebook/jest/issues/8379 + beforeAll(async () => { await callWhenOnline(async () => { servers = await createKibanaServer(); @@ -28,6 +34,7 @@ describe.skip('Example contract tests', () => { }); }); }); + afterAll(async () => { if (servers) { await servers.shutdown(); @@ -37,6 +44,8 @@ describe.skip('Example contract tests', () => { beforeEach(async () => await callWhenOnline(() => esArchiver.load('example'))); afterEach(async () => await callWhenOnline(() => esArchiver.unload('example'))); + */ + it('should run online or offline', async () => { const res = await memorize('example_test_snapshot', async () => { return { serverExists: !!servers.kbnServer };