diff --git a/docs/development/core/public/kibana-plugin-public.capabilities.catalogue.md b/docs/development/core/public/kibana-plugin-public.capabilities.catalogue.md new file mode 100644 index 000000000000..26d4afaec0cc --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.capabilities.catalogue.md @@ -0,0 +1,11 @@ +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [Capabilities](./kibana-plugin-public.capabilities.md) > [catalogue](./kibana-plugin-public.capabilities.catalogue.md) + +## Capabilities.catalogue property + +Catalogue capabilities. Catalogue entries drive the visibility of the Kibana homepage options. + +Signature: + +```typescript +catalogue: Record; +``` diff --git a/docs/development/core/public/kibana-plugin-public.capabilities.management.md b/docs/development/core/public/kibana-plugin-public.capabilities.management.md new file mode 100644 index 000000000000..d225f1c2ba74 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.capabilities.management.md @@ -0,0 +1,13 @@ +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [Capabilities](./kibana-plugin-public.capabilities.md) > [management](./kibana-plugin-public.capabilities.management.md) + +## Capabilities.management property + +Management section capabilities. + +Signature: + +```typescript +management: { + [sectionId: string]: Record; + }; +``` diff --git a/docs/development/core/public/kibana-plugin-public.capabilities.md b/docs/development/core/public/kibana-plugin-public.capabilities.md new file mode 100644 index 000000000000..7ea53e248160 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.capabilities.md @@ -0,0 +1,20 @@ +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [Capabilities](./kibana-plugin-public.capabilities.md) + +## Capabilities interface + +The read-only set of capabilities available for the current UI session. Capabilities are simple key-value pairs of (string, boolean), where the string denotes the capability ID, and the boolean is a flag indicating if the capability is enabled or disabled. + +Signature: + +```typescript +export interface Capabilities +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [catalogue](./kibana-plugin-public.capabilities.catalogue.md) | Record<string, boolean> | Catalogue capabilities. Catalogue entries drive the visibility of the Kibana homepage options. | +| [management](./kibana-plugin-public.capabilities.management.md) | {`

` [sectionId: string]: Record<string, boolean>;`

` } | Management section capabilities. | +| [navLinks](./kibana-plugin-public.capabilities.navlinks.md) | Record<string, boolean> | Navigation link capabilities. | + diff --git a/docs/development/core/public/kibana-plugin-public.capabilities.navlinks.md b/docs/development/core/public/kibana-plugin-public.capabilities.navlinks.md new file mode 100644 index 000000000000..b837dbd5dd54 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.capabilities.navlinks.md @@ -0,0 +1,11 @@ +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [Capabilities](./kibana-plugin-public.capabilities.md) > [navLinks](./kibana-plugin-public.capabilities.navlinks.md) + +## Capabilities.navLinks property + +Navigation link capabilities. + +Signature: + +```typescript +navLinks: Record; +``` diff --git a/docs/development/core/public/kibana-plugin-public.capabilitiessetup.getcapabilities.md b/docs/development/core/public/kibana-plugin-public.capabilitiessetup.getcapabilities.md new file mode 100644 index 000000000000..43fcaacaab04 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.capabilitiessetup.getcapabilities.md @@ -0,0 +1,11 @@ +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [CapabilitiesSetup](./kibana-plugin-public.capabilitiessetup.md) > [getCapabilities](./kibana-plugin-public.capabilitiessetup.getcapabilities.md) + +## CapabilitiesSetup.getCapabilities property + +Gets the read-only capabilities. + +Signature: + +```typescript +getCapabilities: () => Capabilities; +``` diff --git a/docs/development/core/public/kibana-plugin-public.capabilitiessetup.md b/docs/development/core/public/kibana-plugin-public.capabilitiessetup.md new file mode 100644 index 000000000000..ab5b239e587c --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.capabilitiessetup.md @@ -0,0 +1,18 @@ +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [CapabilitiesSetup](./kibana-plugin-public.capabilitiessetup.md) + +## CapabilitiesSetup interface + +Capabilities Setup. + +Signature: + +```typescript +export interface CapabilitiesSetup +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [getCapabilities](./kibana-plugin-public.capabilitiessetup.getcapabilities.md) | () => Capabilities | Gets the read-only capabilities. | + diff --git a/docs/development/core/public/kibana-plugin-public.coresetup.capabilities.md b/docs/development/core/public/kibana-plugin-public.coresetup.capabilities.md new file mode 100644 index 000000000000..5e1863af09c9 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.coresetup.capabilities.md @@ -0,0 +1,9 @@ +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreSetup](./kibana-plugin-public.coresetup.md) > [capabilities](./kibana-plugin-public.coresetup.capabilities.md) + +## CoreSetup.capabilities property + +Signature: + +```typescript +capabilities: CapabilitiesSetup; +``` diff --git a/docs/development/core/public/kibana-plugin-public.coresetup.md b/docs/development/core/public/kibana-plugin-public.coresetup.md index a34c13b94412..13384b87d5d8 100644 --- a/docs/development/core/public/kibana-plugin-public.coresetup.md +++ b/docs/development/core/public/kibana-plugin-public.coresetup.md @@ -15,6 +15,7 @@ export interface CoreSetup | Property | Type | Description | | --- | --- | --- | | [basePath](./kibana-plugin-public.coresetup.basepath.md) | BasePathSetup | | +| [capabilities](./kibana-plugin-public.coresetup.capabilities.md) | CapabilitiesSetup | | | [chrome](./kibana-plugin-public.coresetup.chrome.md) | ChromeSetup | | | [fatalErrors](./kibana-plugin-public.coresetup.fatalerrors.md) | FatalErrorsSetup | | | [http](./kibana-plugin-public.coresetup.http.md) | HttpSetup | | diff --git a/docs/development/core/public/kibana-plugin-public.md b/docs/development/core/public/kibana-plugin-public.md index 372592c9f581..485eb4a0700a 100644 --- a/docs/development/core/public/kibana-plugin-public.md +++ b/docs/development/core/public/kibana-plugin-public.md @@ -14,6 +14,8 @@ | Interface | Description | | --- | --- | +| [Capabilities](./kibana-plugin-public.capabilities.md) | The read-only set of capabilities available for the current UI session. Capabilities are simple key-value pairs of (string, boolean), where the string denotes the capability ID, and the boolean is a flag indicating if the capability is enabled or disabled. | +| [CapabilitiesSetup](./kibana-plugin-public.capabilitiessetup.md) | Capabilities Setup. | | [ChromeBrand](./kibana-plugin-public.chromebrand.md) | | | [ChromeBreadcrumb](./kibana-plugin-public.chromebreadcrumb.md) | | | [CoreSetup](./kibana-plugin-public.coresetup.md) | Core services exposed to the start lifecycle | diff --git a/docs/development/security/rbac.asciidoc b/docs/development/security/rbac.asciidoc index 916ec72ab76c..6feb499f5887 100644 --- a/docs/development/security/rbac.asciidoc +++ b/docs/development/security/rbac.asciidoc @@ -32,9 +32,9 @@ Authorization: Basic kibana changeme "actions":[ "version:7.0.0-alpha1-SNAPSHOT", "action:login", - "action:saved_objects/dashboard/get", - "action:saved_objects/dashboard/bulk_get", - "action:saved_objects/dashboard/find", + "saved_object:dashboard/get", + "saved_object:dashboard/bulk_get", + "saved_object:dashboard/find", ... ],"metadata":{}} } @@ -90,7 +90,7 @@ Authorization: Basic foo_read_only_user password "application":"kibana-.kibana", "resources":["*"], "privileges":[ - "action:saved_objects/dashboard/save", + "saved_object:dashboard/save", ] } ] @@ -120,7 +120,7 @@ Authorization: Basic foo_legacy_user password "application":"kibana-.kibana", "resources":["*"], "privileges":[ - "action:saved_objects/dashboard/save" + "saved_object:dashboard/save" ] } ], @@ -152,7 +152,7 @@ Here is an example response if the user does not have application privileges, bu "application": { "kibana-.kibana": { "*": { - "action:saved_objects/dashboard/save": false + "saved_object:dashboard/save": false } } } diff --git a/packages/kbn-plugin-generator/sao_template/sao.test.js b/packages/kbn-plugin-generator/sao_template/sao.test.js index 7d12041cf025..77c9b4533b9c 100755 --- a/packages/kbn-plugin-generator/sao_template/sao.test.js +++ b/packages/kbn-plugin-generator/sao_template/sao.test.js @@ -55,6 +55,7 @@ describe('plugin generator sao integration', () => { expect(uiExports).not.toContain('app:'); expect(uiExports).not.toContain('hacks:'); expect(uiExports).not.toContain('init(server, options)'); + expect(uiExports).not.toContain('registerFeature('); }); it('includes app when answering yes', async () => { @@ -73,8 +74,9 @@ describe('plugin generator sao integration', () => { const uiExports = getConfig(res.files['index.js']); expect(uiExports).toContain('app:'); + expect(uiExports).toContain('init(server, options)'); + expect(uiExports).toContain('registerFeature('); expect(uiExports).not.toContain('hacks:'); - expect(uiExports).not.toContain('init(server, options)'); }); it('includes hack when answering yes', async () => { @@ -94,7 +96,8 @@ describe('plugin generator sao integration', () => { const uiExports = getConfig(res.files['index.js']); expect(uiExports).toContain('app:'); expect(uiExports).toContain('hacks:'); - expect(uiExports).not.toContain('init(server, options)'); + expect(uiExports).toContain('init(server, options)'); + expect(uiExports).toContain('registerFeature('); }); it('includes server api when answering yes', async () => { @@ -115,6 +118,7 @@ describe('plugin generator sao integration', () => { expect(uiExports).toContain('app:'); expect(uiExports).toContain('hacks:'); expect(uiExports).toContain('init(server, options)'); + expect(uiExports).toContain('registerFeature('); }); it('plugin config has correct name and main path', async () => { diff --git a/packages/kbn-plugin-generator/sao_template/template/index.js b/packages/kbn-plugin-generator/sao_template/template/index.js index 788ea2d3e338..8377ff8d074c 100755 --- a/packages/kbn-plugin-generator/sao_template/template/index.js +++ b/packages/kbn-plugin-generator/sao_template/template/index.js @@ -3,6 +3,11 @@ import { resolve } from 'path'; import { existsSync } from 'fs'; <% } -%> + +<% if (generateApp) { -%> +import { i18n } from '@kbn/i18n'; +<% } -%> + <% if (generateApi) { -%> import exampleRoute from './server/routes/example'; @@ -34,11 +39,49 @@ export default function (kibana) { enabled: Joi.boolean().default(true), }).default(); }, - <%_ if (generateApi) { -%> + <%_ if (generateApi || generateApp) { -%> init(server, options) { // eslint-disable-line no-unused-vars + <%_ if (generateApp) { -%> + const xpackMainPlugin = server.plugins.xpack_main; + if (xpackMainPlugin) { + const featureId = '<%= snakeCase(name) %>'; + + xpackMainPlugin.registerFeature({ + id: featureId, + name: i18n.translate('<%= camelCase(name) %>.featureRegistry.featureName', { + defaultMessage: '<%= name %>', + }), + navLinkId: featureId, + icon: 'questionInCircle', + app: [featureId, 'kibana'], + catalogue: [], + privileges: { + all: { + api: [], + savedObject: { + all: [], + read: ['config'], + }, + ui: ['show'], + }, + read: { + api: [], + savedObject: { + all: [], + read: ['config'], + }, + ui: ['show'], + }, + }, + }); + } + <%_ } -%> + + <%_ if (generateApi) { -%> // Add server routes and initialize the plugin here exampleRoute(server); + <%_ } -%> } <%_ } -%> }); diff --git a/packages/kbn-ui-framework/src/components/table/listing_table/listing_table.js b/packages/kbn-ui-framework/src/components/table/listing_table/listing_table.js index ad15d94b345c..ba3bf91fcdf1 100644 --- a/packages/kbn-ui-framework/src/components/table/listing_table/listing_table.js +++ b/packages/kbn-ui-framework/src/components/table/listing_table/listing_table.js @@ -47,6 +47,7 @@ export function KuiListingTable({ toolBarActions, onFilter, onItemSelectionChanged, + enableSelection, selectedRowIds, filter, prompt, @@ -76,11 +77,12 @@ export function KuiListingTable({ } } - function renderTableRows() { + function renderTableRows(enableSelection) { return rows.map((row, rowIndex) => { return ( = 0} onSelectionChanged={toggleRow} row={row} @@ -111,15 +113,17 @@ export function KuiListingTable({ return ( - + {enableSelection && + + } {renderHeader()} - {renderTableRows()} + {renderTableRows(enableSelection)} ); @@ -171,6 +175,7 @@ KuiListingTable.propTypes = { })), pager: PropTypes.node, onItemSelectionChanged: PropTypes.func.isRequired, + enableSelection: PropTypes.bool, selectedRowIds: PropTypes.array, prompt: PropTypes.node, // If given, will be shown instead of a table with rows. onFilter: PropTypes.func, @@ -181,4 +186,5 @@ KuiListingTable.propTypes = { KuiListingTable.defaultProps = { rows: [], selectedRowIds: [], + enableSelection: true, }; diff --git a/packages/kbn-ui-framework/src/components/table/listing_table/listing_table_row.js b/packages/kbn-ui-framework/src/components/table/listing_table/listing_table_row.js index 212ccfd70363..f332e35c9b74 100644 --- a/packages/kbn-ui-framework/src/components/table/listing_table/listing_table_row.js +++ b/packages/kbn-ui-framework/src/components/table/listing_table/listing_table_row.js @@ -57,13 +57,15 @@ export class KuiListingTableRow extends React.PureComponent { } render() { - const { isSelected } = this.props; + const { enableSelection, isSelected } = this.props; return ( - + {enableSelection && + + } {this.renderCells()} ); @@ -83,6 +85,11 @@ KuiListingTableRow.propTypes = { ], )), }).isRequired, + enableSelection: PropTypes.bool, onSelectionChanged: PropTypes.func.isRequired, isSelected: PropTypes.bool, }; + +KuiListingTableRow.defaultProps = { + enableSelection: true, +}; diff --git a/src/core/public/capabilities/capabilities_service.mock.ts b/src/core/public/capabilities/capabilities_service.mock.ts new file mode 100644 index 000000000000..980755302f38 --- /dev/null +++ b/src/core/public/capabilities/capabilities_service.mock.ts @@ -0,0 +1,45 @@ +/* + * 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 { Capabilities, CapabilitiesService, CapabilitiesSetup } from './capabilities_service'; + +const createSetupContractMock = () => { + const setupContract: jest.Mocked = { + getCapabilities: jest.fn(), + }; + setupContract.getCapabilities.mockReturnValue({ + catalogue: {}, + management: {}, + navLinks: {}, + } as Capabilities); + return setupContract; +}; + +type CapabilitiesServiceContract = PublicMethodsOf; +const createMock = () => { + const mocked: jest.Mocked = { + setup: jest.fn(), + }; + mocked.setup.mockReturnValue(createSetupContractMock()); + return mocked; +}; + +export const capabilitiesServiceMock = { + create: createMock, + createSetupContract: createSetupContractMock, +}; diff --git a/src/core/public/capabilities/capabilities_service.test.ts b/src/core/public/capabilities/capabilities_service.test.ts new file mode 100644 index 000000000000..9c138b780f56 --- /dev/null +++ b/src/core/public/capabilities/capabilities_service.test.ts @@ -0,0 +1,60 @@ +/* + * 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 { InjectedMetadataService } from '../injected_metadata'; +import { CapabilitiesService } from './capabilities_service'; + +describe('#start', () => { + it('returns a service with getCapabilities', () => { + const injectedMetadata = new InjectedMetadataService({ + injectedMetadata: { + vars: { + uiCapabilities: { + foo: 'bar', + bar: 'baz', + }, + }, + } as any, + }); + const service = new CapabilitiesService(); + const startContract = service.setup({ injectedMetadata: injectedMetadata.setup() }); + expect(startContract.getCapabilities()).toEqual({ + foo: 'bar', + bar: 'baz', + }); + }); + + it(`does not allow Capabilities to be modified`, () => { + const injectedMetadata = new InjectedMetadataService({ + injectedMetadata: { + vars: { + uiCapabilities: { + foo: 'bar', + bar: 'baz', + }, + }, + } as any, + }); + const service = new CapabilitiesService(); + const startContract = service.setup({ injectedMetadata: injectedMetadata.setup() }); + const capabilities = startContract.getCapabilities(); + + // @ts-ignore TypeScript knows this shouldn't be possible + expect(() => (capabilities.foo = 'foo')).toThrowError(); + }); +}); diff --git a/src/core/public/capabilities/capabilities_service.tsx b/src/core/public/capabilities/capabilities_service.tsx new file mode 100644 index 000000000000..560b8f628ee3 --- /dev/null +++ b/src/core/public/capabilities/capabilities_service.tsx @@ -0,0 +1,72 @@ +/* + * 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 { InjectedMetadataSetup } from '../injected_metadata'; +import { deepFreeze } from '../utils/deep_freeze'; + +interface StartDeps { + injectedMetadata: InjectedMetadataSetup; +} + +/** + * The read-only set of capabilities available for the current UI session. + * Capabilities are simple key-value pairs of (string, boolean), where the string denotes the capability ID, + * and the boolean is a flag indicating if the capability is enabled or disabled. + * + * @public + */ +export interface Capabilities { + /** Navigation link capabilities. */ + navLinks: Record; + + /** Management section capabilities. */ + management: { + [sectionId: string]: Record; + }; + + /** Catalogue capabilities. Catalogue entries drive the visibility of the Kibana homepage options. */ + catalogue: Record; + + /** Custom capabilities, registered by plugins. */ + [key: string]: Record>; +} + +/** + * Capabilities Setup. + * @public + */ +export interface CapabilitiesSetup { + /** + * Gets the read-only capabilities. + */ + getCapabilities: () => Capabilities; +} + +/** @internal */ + +/** + * Service that is responsible for UI Capabilities. + */ +export class CapabilitiesService { + public setup({ injectedMetadata }: StartDeps): CapabilitiesSetup { + return { + getCapabilities: () => + deepFreeze(injectedMetadata.getInjectedVar('uiCapabilities') as Capabilities), + }; + } +} diff --git a/src/core/public/capabilities/index.ts b/src/core/public/capabilities/index.ts new file mode 100644 index 000000000000..dd95d8530a5b --- /dev/null +++ b/src/core/public/capabilities/index.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ + +export { Capabilities, CapabilitiesService, CapabilitiesSetup } from './capabilities_service'; diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 8d3bf0132234..9cce85539727 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -23,6 +23,7 @@ import { Subject } from 'rxjs'; import { CoreSetup } from '.'; import { BasePathService } from './base_path'; +import { CapabilitiesService } from './capabilities'; import { ChromeService } from './chrome'; import { FatalErrorsService } from './fatal_errors'; import { HttpService } from './http'; @@ -64,6 +65,7 @@ export class CoreSystem { private readonly basePath: BasePathService; private readonly chrome: ChromeService; private readonly i18n: I18nService; + private readonly capabilities: CapabilitiesService; private readonly overlay: OverlayService; private readonly plugins: PluginsService; @@ -85,6 +87,8 @@ export class CoreSystem { this.i18n = new I18nService(); + this.capabilities = new CapabilitiesService(); + this.injectedMetadata = new InjectedMetadataService({ injectedMetadata, }); @@ -128,6 +132,7 @@ export class CoreSystem { const http = this.http.setup({ fatalErrors }); const overlays = this.overlay.setup({ i18n }); const basePath = this.basePath.setup({ injectedMetadata }); + const capabilities = this.capabilities.setup({ injectedMetadata }); const uiSettings = this.uiSettings.setup({ notifications, http, @@ -145,6 +150,7 @@ export class CoreSystem { fatalErrors, http, i18n, + capabilities, injectedMetadata, notifications, uiSettings, diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 4aa0b533cc39..d25d0c105901 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -18,6 +18,7 @@ */ import { BasePathSetup } from './base_path'; +import { Capabilities, CapabilitiesSetup } from './capabilities'; import { ChromeBrand, ChromeBreadcrumb, ChromeHelpExtension, ChromeSetup } from './chrome'; import { FatalErrorsSetup } from './fatal_errors'; import { HttpSetup } from './http'; @@ -42,6 +43,7 @@ export interface CoreSetup { notifications: NotificationsSetup; http: HttpSetup; basePath: BasePathSetup; + capabilities: CapabilitiesSetup; uiSettings: UiSettingsSetup; chrome: ChromeSetup; overlays: OverlaySetup; @@ -52,6 +54,8 @@ export { HttpSetup, FatalErrorsSetup, I18nSetup, + CapabilitiesSetup, + Capabilities, ChromeSetup, ChromeBreadcrumb, ChromeBrand, diff --git a/src/core/public/injected_metadata/injected_metadata_service.ts b/src/core/public/injected_metadata/injected_metadata_service.ts index 85001fbd3de3..b7b76f195e74 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.ts @@ -20,7 +20,7 @@ import { get } from 'lodash'; import { DiscoveredPlugin, PluginName } from '../../server'; import { UiSettingsState } from '../ui_settings'; -import { deepFreeze } from './deep_freeze'; +import { deepFreeze } from '../utils/deep_freeze'; /** @internal */ export interface InjectedMetadataParams { diff --git a/src/core/public/kibana.api.md b/src/core/public/kibana.api.md index 9dca4df6a0c5..85d4d86fda4a 100644 --- a/src/core/public/kibana.api.md +++ b/src/core/public/kibana.api.md @@ -16,6 +16,21 @@ import { Toast } from '@elastic/eui'; // @public (undocumented) export type BasePathSetup = ReturnType; +// @public +export interface Capabilities { + [key: string]: Record>; + catalogue: Record; + management: { + [sectionId: string]: Record; + }; + navLinks: Record; +} + +// @public +export interface CapabilitiesSetup { + getCapabilities: () => Capabilities; +} + // @public (undocumented) export interface ChromeBrand { // (undocumented) @@ -53,6 +68,8 @@ export interface CoreSetup { // (undocumented) basePath: BasePathSetup; // (undocumented) + capabilities: CapabilitiesSetup; + // (undocumented) chrome: ChromeSetup; // (undocumented) fatalErrors: FatalErrorsSetup; diff --git a/src/core/public/legacy/__snapshots__/legacy_service.test.ts.snap b/src/core/public/legacy/__snapshots__/legacy_service.test.ts.snap index c1af66257c67..096ef3addfed 100644 --- a/src/core/public/legacy/__snapshots__/legacy_service.test.ts.snap +++ b/src/core/public/legacy/__snapshots__/legacy_service.test.ts.snap @@ -6,6 +6,7 @@ Array [ "ui/i18n", "ui/notify/fatal_error", "ui/notify/toasts", + "ui/capabilities", "ui/chrome/api/loading_count", "ui/chrome/api/base_path", "ui/chrome/api/ui_settings", @@ -26,6 +27,7 @@ Array [ "ui/i18n", "ui/notify/fatal_error", "ui/notify/toasts", + "ui/capabilities", "ui/chrome/api/loading_count", "ui/chrome/api/base_path", "ui/chrome/api/ui_settings", diff --git a/src/core/public/legacy/legacy_service.test.ts b/src/core/public/legacy/legacy_service.test.ts index 1aadf271f5e5..7ee06f76b80d 100644 --- a/src/core/public/legacy/legacy_service.test.ts +++ b/src/core/public/legacy/legacy_service.test.ts @@ -53,6 +53,14 @@ jest.mock('ui/i18n', () => { }; }); +const mockUICapabilitiesInit = jest.fn(); +jest.mock('ui/capabilities', () => { + mockLoadOrder.push('ui/capabilities'); + return { + __newPlatformInit__: mockUICapabilitiesInit, + }; +}); + const mockFatalErrorInit = jest.fn(); jest.mock('ui/notify/fatal_error', () => { mockLoadOrder.push('ui/notify/fatal_error'); @@ -142,6 +150,7 @@ jest.mock('ui/chrome/services/global_nav_state', () => { }); import { basePathServiceMock } from '../base_path/base_path_service.mock'; +import { capabilitiesServiceMock } from '../capabilities/capabilities_service.mock'; import { chromeServiceMock } from '../chrome/chrome_service.mock'; import { fatalErrorsServiceMock } from '../fatal_errors/fatal_errors_service.mock'; import { httpServiceMock } from '../http/http_service.mock'; @@ -159,6 +168,7 @@ const httpSetup = httpServiceMock.createSetupContract(); const i18nSetup = i18nServiceMock.createSetupContract(); const injectedMetadataSetup = injectedMetadataServiceMock.createSetupContract(); const notificationsSetup = notificationServiceMock.createSetupContract(); +const capabilitiesSetup = capabilitiesServiceMock.createSetupContract(); const uiSettingsSetup = uiSettingsServiceMock.createSetupContract(); const overlaySetup = overlayServiceMock.createSetupContract(); @@ -176,6 +186,7 @@ const defaultSetupDeps = { notifications: notificationsSetup, http: httpSetup, basePath: basePathSetup, + capabilities: capabilitiesSetup, uiSettings: uiSettingsSetup, chrome: chromeSetup, overlays: overlaySetup, @@ -215,6 +226,17 @@ describe('#setup()', () => { expect(mockI18nContextInit).toHaveBeenCalledWith(i18nSetup.Context); }); + it('passes uiCapabilities to ui/capabilities', () => { + const legacyPlatform = new LegacyPlatformService({ + ...defaultParams, + }); + + legacyPlatform.setup(defaultSetupDeps); + + expect(mockUICapabilitiesInit).toHaveBeenCalledTimes(1); + expect(mockUICapabilitiesInit).toHaveBeenCalledWith(capabilitiesSetup); + }); + it('passes fatalErrors service to ui/notify/fatal_errors', () => { const legacyPlatform = new LegacyPlatformService({ ...defaultParams, diff --git a/src/core/public/legacy/legacy_service.ts b/src/core/public/legacy/legacy_service.ts index 2095b7fb60e8..e503b1b05013 100644 --- a/src/core/public/legacy/legacy_service.ts +++ b/src/core/public/legacy/legacy_service.ts @@ -44,6 +44,7 @@ export class LegacyPlatformService { notifications, http, basePath, + capabilities, uiSettings, chrome, } = core; @@ -54,6 +55,7 @@ export class LegacyPlatformService { require('ui/i18n').__newPlatformInit__(i18n.Context); require('ui/notify/fatal_error').__newPlatformInit__(fatalErrors); require('ui/notify/toasts').__newPlatformInit__(notifications.toasts); + require('ui/capabilities').__newPlatformInit__(capabilities); require('ui/chrome/api/loading_count').__newPlatformInit__(http); require('ui/chrome/api/base_path').__newPlatformInit__(basePath); require('ui/chrome/api/ui_settings').__newPlatformInit__(uiSettings); diff --git a/src/core/public/injected_metadata/deep_freeze.test.ts b/src/core/public/utils/deep_freeze.test.ts similarity index 100% rename from src/core/public/injected_metadata/deep_freeze.test.ts rename to src/core/public/utils/deep_freeze.test.ts diff --git a/src/core/public/injected_metadata/deep_freeze.ts b/src/core/public/utils/deep_freeze.ts similarity index 100% rename from src/core/public/injected_metadata/deep_freeze.ts rename to src/core/public/utils/deep_freeze.ts diff --git a/src/core/public/injected_metadata/integration_tests/__fixtures__/frozen_object_mutation/index.ts b/src/core/public/utils/integration_tests/__fixtures__/frozen_object_mutation/index.ts similarity index 100% rename from src/core/public/injected_metadata/integration_tests/__fixtures__/frozen_object_mutation/index.ts rename to src/core/public/utils/integration_tests/__fixtures__/frozen_object_mutation/index.ts diff --git a/src/core/public/injected_metadata/integration_tests/__fixtures__/frozen_object_mutation/tsconfig.json b/src/core/public/utils/integration_tests/__fixtures__/frozen_object_mutation/tsconfig.json similarity index 100% rename from src/core/public/injected_metadata/integration_tests/__fixtures__/frozen_object_mutation/tsconfig.json rename to src/core/public/utils/integration_tests/__fixtures__/frozen_object_mutation/tsconfig.json diff --git a/src/core/public/injected_metadata/integration_tests/deep_freeze.test.ts b/src/core/public/utils/integration_tests/deep_freeze.test.ts similarity index 100% rename from src/core/public/injected_metadata/integration_tests/deep_freeze.test.ts rename to src/core/public/utils/integration_tests/deep_freeze.test.ts diff --git a/src/es_archiver/lib/indices/kibana_index.js b/src/es_archiver/lib/indices/kibana_index.js index d0477186aee0..cbb3cc9f9cbd 100644 --- a/src/es_archiver/lib/indices/kibana_index.js +++ b/src/es_archiver/lib/indices/kibana_index.js @@ -150,6 +150,7 @@ export async function createDefaultSpace({ index, client }) { space: { name: 'Default Space', description: 'This is the default space', + disabledFeatures: [], _reserved: true } } diff --git a/src/legacy/core_plugins/console/index.js b/src/legacy/core_plugins/console/index.js index 652696bc7bfd..fa56584d9cab 100644 --- a/src/legacy/core_plugins/console/index.js +++ b/src/legacy/core_plugins/console/index.js @@ -110,7 +110,12 @@ export default function (kibana) { defaultVars = { elasticsearchUrl: url.format( Object.assign(url.parse(head(legacyEsConfig.hosts)), { auth: false }) - ) + ), + uiCapabilities: { + dev_tools: { + show: true + }, + } }; server.route(createProxyRoute({ diff --git a/src/legacy/core_plugins/console/public/console.js b/src/legacy/core_plugins/console/public/console.js index a623927b9308..f5ba87722e1e 100644 --- a/src/legacy/core_plugins/console/public/console.js +++ b/src/legacy/core_plugins/console/public/console.js @@ -23,6 +23,7 @@ import template from './index.html'; require('brace'); require('ui/autoload/styles'); +require('ui/capabilities/route_setup'); require('./src/controllers/sense_controller'); require('./src/directives/sense_history'); @@ -33,6 +34,7 @@ require('./src/directives/console_menu_directive'); uiRoutes.when('/dev_tools/console', { + requireUICapability: 'dev_tools.show', controller: 'SenseController', template, }); diff --git a/src/legacy/core_plugins/console/server/proxy_route.js b/src/legacy/core_plugins/console/server/proxy_route.js index 963f26bd3523..b43514b781a4 100644 --- a/src/legacy/core_plugins/console/server/proxy_route.js +++ b/src/legacy/core_plugins/console/server/proxy_route.js @@ -65,6 +65,7 @@ export const createProxyRoute = ({ path: '/api/console/proxy', method: 'POST', config: { + tags: ['access:console'], payload: { output: 'stream', parse: false diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index b2a9678fe235..af78119c046a 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -138,9 +138,61 @@ export default function (kibana) { }, injectDefaultVars(server, options) { + const { savedObjects } = server; + return { kbnIndex: options.index, - kbnBaseUrl + kbnBaseUrl, + uiCapabilities: { + discover: { + show: true, + createShortUrl: true, + save: true, + }, + visualize: { + show: true, + createShortUrl: true, + delete: true, + save: true, + }, + dashboard: { + createNew: true, + show: true, + showWriteControls: true, + }, + catalogue: { + discover: true, + dashboard: true, + visualize: true, + console: true, + advanced_settings: true, + index_patterns: true, + }, + advancedSettings: { + save: true + }, + indexPatterns: { + createNew: true, + }, + savedObjectsManagement: savedObjects.types.reduce((acc, type) => ({ + ...acc, + [type]: { + delete: true, + edit: true, + read: true, + } + }), {}), + management: { + /* + * Management settings correspond to management section/link ids, and should not be changed + * without also updating those definitions. + */ + kibana: { + settings: true, + index_patterns: true, + }, + } + } }; }, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.js b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.js index a4fcbe27695b..e6ba5c828114 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.js @@ -500,6 +500,7 @@ app.directive('dashboardApp', function ($injector) { showShareContextMenu({ anchorElement, allowEmbed: true, + allowShortUrl: !dashboardConfig.getHideWriteControls(), getUnhashableStates, objectId: dash.id, objectType: 'dashboard', diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_config.js b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_config.js index 8267e7626fb9..7179f810db08 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_config.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_config.js @@ -18,9 +18,11 @@ */ import { uiModules } from 'ui/modules'; +import { uiCapabilities } from 'ui/capabilities'; + uiModules.get('kibana') .provider('dashboardConfig', () => { - let hideWriteControls = false; + let hideWriteControls = !uiCapabilities.dashboard.showWriteControls; return { /** diff --git a/src/legacy/core_plugins/kibana/public/dashboard/index.js b/src/legacy/core_plugins/kibana/public/dashboard/index.js index 000f5e840a8d..98c2d4f48e89 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/index.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/index.js @@ -37,6 +37,7 @@ import { recentlyAccessed } from 'ui/persisted_log'; import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; import { DashboardListing, EMPTY_FILTER } from './listing/dashboard_listing'; import { uiModules } from 'ui/modules'; +import 'ui/capabilities/route_setup'; const app = uiModules.get('app/dashboard', [ 'ngRoute', @@ -55,7 +56,8 @@ function createNewDashboardCtrl($scope, i18n) { uiRoutes .defaults(/dashboard/, { - requireDefaultIndex: true + requireDefaultIndex: true, + requireUICapability: 'dashboard.show' }) .when(DashboardConstants.LANDING_PAGE_PATH, { template: dashboardListingTemplate, @@ -117,6 +119,7 @@ uiRoutes .when(DashboardConstants.CREATE_NEW_DASHBOARD_URL, { template: dashboardTemplate, controller: createNewDashboardCtrl, + requireUICapability: 'dashboard.createNew', resolve: { dash: function (savedDashboards, redirectWhenMissing) { return savedDashboards.get() diff --git a/src/legacy/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap b/src/legacy/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap index 4d860b8843fb..ce81f5f2e613 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap @@ -2,13 +2,12 @@ exports[`after fetch hideWriteControls 1`] = ` , isDisabled: ({ embeddable }) => !embeddable || !embeddable.metadata || !embeddable.metadata.editUrl, - isVisible: ({ containerState }) => containerState.viewMode === DashboardViewMode.EDIT, + isVisible: ({ containerState, embeddable }) => { + const canEditEmbeddable = Boolean( + embeddable && embeddable.metadata && embeddable.metadata.editable + ); + const inDashboardEditMode = containerState.viewMode === DashboardViewMode.EDIT; + return canEditEmbeddable && inDashboardEditMode; + }, getHref: ({ embeddable }) => { if (embeddable && embeddable.metadata.editUrl) { return embeddable.metadata.editUrl; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.js b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.js index febf53de9670..9984153fd30a 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.js @@ -21,6 +21,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { uiCapabilities } from 'ui/capabilities'; import { toastNotifications } from 'ui/notify'; import { SavedObjectFinder } from 'ui/saved_objects/components/saved_object_finder'; @@ -87,18 +88,20 @@ export class DashboardAddPanel extends React.Component { )} /> - - - - - - - - - + { uiCapabilities.visualize.save ? ( + + + + + + + + + + ) : null } ); } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.test.js b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.test.js index eccf9198939e..8d98fc8cfe80 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.test.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.test.js @@ -25,6 +25,16 @@ import { DashboardAddPanel, } from './add_panel'; +jest.mock('ui/capabilities', + () => ({ + uiCapabilities: { + visualize: { + show: true, + save: true + } + } + }), { virtual: true }); + jest.mock('ui/notify', () => ({ toastNotifications: { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/get_top_nav_config.js b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/get_top_nav_config.js index 17cb96eba7f9..e46cf388f5a6 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/get_top_nav_config.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/get_top_nav_config.js @@ -35,7 +35,8 @@ export function getTopNavConfig(dashboardMode, actions, hideWriteControls) { return ( hideWriteControls ? [ - getFullScreenConfig(actions[TopNavIds.FULL_SCREEN]) + getFullScreenConfig(actions[TopNavIds.FULL_SCREEN]), + getShareConfig(actions[TopNavIds.SHARE]), ] : [ getFullScreenConfig(actions[TopNavIds.FULL_SCREEN]), diff --git a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field.js b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field.js index 1f4b3103af7c..a27f63d89cb4 100644 --- a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field.js +++ b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field.js @@ -24,6 +24,7 @@ import 'ui/directives/css_truncate'; import 'ui/directives/field_name'; import './string_progress_bar'; import detailsHtml from './lib/detail_views/string.html'; +import { uiCapabilities } from 'ui/capabilities'; import { uiModules } from 'ui/modules'; const app = uiModules.get('apps/discover'); @@ -76,6 +77,8 @@ app.directive('discoverField', function ($compile, i18n) { }; + $scope.canVisualize = uiCapabilities.visualize.show; + $scope.toggleDisplay = function (field) { if (field.display) { $scope.onRemoveField(field.name); diff --git a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/lib/detail_views/string.html b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/lib/detail_views/string.html index 37786c3812ec..5d134911fc91 100644 --- a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/lib/detail_views/string.html +++ b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/lib/detail_views/string.html @@ -87,7 +87,7 @@ diff --git a/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js b/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js index 9b7ce8925cab..463a7bebf14e 100644 --- a/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js @@ -68,6 +68,7 @@ import { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal'; import { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal'; import { getRootBreadcrumbs, getSavedSearchBreadcrumbs } from '../breadcrumbs'; import { buildVislibDimensions } from 'ui/visualize/loader/pipeline_helpers/build_pipeline'; +import 'ui/capabilities/route_setup'; const fetchStatuses = { UNINITIALIZED: 'uninitialized', @@ -85,12 +86,13 @@ const app = uiModules.get('apps/discover', [ uiRoutes .defaults(/^\/discover(\/|$)/, { requireDefaultIndex: true, + requireUICapability: 'discover.show', k7Breadcrumbs: ($route, $injector) => $injector.invoke( $route.current.params.id ? getSavedSearchBreadcrumbs : getRootBreadcrumbs - ) + ), }) .when('/discover/:id?', { template: indexTemplate, @@ -173,6 +175,7 @@ function discoverController( kbnUrl, localStorage, i18n, + uiCapabilities, ) { const visualizeLoader = Private(VisualizeLoaderProvider); let visualizeHandler; @@ -211,110 +214,131 @@ function discoverController( dirty: !savedSearch.id }; - $scope.topNavMenu = [{ - key: 'new', - label: i18n('kbn.discover.localMenu.localMenu.newSearchTitle', { - defaultMessage: 'New', - }), - description: i18n('kbn.discover.localMenu.newSearchDescription', { - defaultMessage: 'New Search', - }), - run: function () { kbnUrl.change('/discover'); }, - testId: 'discoverNewButton', - }, { - key: 'save', - label: i18n('kbn.discover.localMenu.saveTitle', { - defaultMessage: 'Save', - }), - description: i18n('kbn.discover.localMenu.saveSearchDescription', { - defaultMessage: 'Save Search', - }), - testId: 'discoverSaveButton', - run: async () => { - const onSave = ({ newTitle, newCopyOnSave, isTitleDuplicateConfirmed, onTitleDuplicate }) => { - const currentTitle = savedSearch.title; - savedSearch.title = newTitle; - savedSearch.copyOnSave = newCopyOnSave; - const saveOptions = { - confirmOverwrite: false, - isTitleDuplicateConfirmed, - onTitleDuplicate, + const getTopNavLinks = () => { + const newSearch = { + key: 'new', + label: i18n('kbn.discover.localMenu.localMenu.newSearchTitle', { + defaultMessage: 'New', + }), + description: i18n('kbn.discover.localMenu.newSearchDescription', { + defaultMessage: 'New Search', + }), + run: function () { kbnUrl.change('/discover'); }, + testId: 'discoverNewButton', + }; + + const saveSearch = { + key: 'save', + label: i18n('kbn.discover.localMenu.saveTitle', { + defaultMessage: 'Save', + }), + description: i18n('kbn.discover.localMenu.saveSearchDescription', { + defaultMessage: 'Save Search', + }), + testId: 'discoverSaveButton', + run: async () => { + const onSave = ({ newTitle, newCopyOnSave, isTitleDuplicateConfirmed, onTitleDuplicate }) => { + const currentTitle = savedSearch.title; + savedSearch.title = newTitle; + savedSearch.copyOnSave = newCopyOnSave; + const saveOptions = { + confirmOverwrite: false, + isTitleDuplicateConfirmed, + onTitleDuplicate, + }; + return saveDataSource(saveOptions).then(({ id, error }) => { + // If the save wasn't successful, put the original values back. + if (!id || error) { + savedSearch.title = currentTitle; + } + return { id, error }; + }); }; - return saveDataSource(saveOptions).then(({ id, error }) => { - // If the save wasn't successful, put the original values back. - if (!id || error) { - savedSearch.title = currentTitle; + + const saveModal = ( + { }} + title={savedSearch.title} + showCopyOnSave={savedSearch.id ? true : false} + objectType="search" + />); + showSaveModal(saveModal); + } + }; + + const openSearch = { + key: 'open', + label: i18n('kbn.discover.localMenu.openTitle', { + defaultMessage: 'Open', + }), + description: i18n('kbn.discover.localMenu.openSavedSearchDescription', { + defaultMessage: 'Open Saved Search', + }), + testId: 'discoverOpenButton', + run: () => { + showOpenSearchPanel({ + makeUrl: (searchId) => { + return kbnUrl.eval('#/discover/{{id}}', { id: searchId }); } - return { id, error }; }); - }; + } + }; - const saveModal = ( - {}} - title={savedSearch.title} - showCopyOnSave={savedSearch.id ? true : false} - objectType="search" - />); - showSaveModal(saveModal); - } - }, { - key: 'open', - label: i18n('kbn.discover.localMenu.openTitle', { - defaultMessage: 'Open', - }), - description: i18n('kbn.discover.localMenu.openSavedSearchDescription', { - defaultMessage: 'Open Saved Search', - }), - testId: 'discoverOpenButton', - run: () => { - showOpenSearchPanel({ - makeUrl: (searchId) => { - return kbnUrl.eval('#/discover/{{id}}', { id: searchId }); - } - }); - } - }, { - key: 'share', - label: i18n('kbn.discover.localMenu.shareTitle', { - defaultMessage: 'Share', - }), - description: i18n('kbn.discover.localMenu.shareSearchDescription', { - defaultMessage: 'Share Search', - }), - testId: 'shareTopNavButton', - run: async (menuItem, navController, anchorElement) => { - const sharingData = await this.getSharingData(); - showShareContextMenu({ - anchorElement, - allowEmbed: false, - getUnhashableStates, - objectId: savedSearch.id, - objectType: 'search', - shareContextMenuExtensions, - sharingData: { - ...sharingData, - title: savedSearch.title, - }, - isDirty: $appStatus.dirty, - }); - } - }, { - key: 'inspect', - label: i18n('kbn.discover.localMenu.inspectTitle', { - defaultMessage: 'Inspect', - }), - description: i18n('kbn.discover.localMenu.openInspectorForSearchDescription', { - defaultMessage: 'Open Inspector for search', - }), - testId: 'openInspectorButton', - run() { - Inspector.open(inspectorAdapters, { - title: savedSearch.title - }); - } - }]; + const shareSearch = { + key: 'share', + label: i18n('kbn.discover.localMenu.shareTitle', { + defaultMessage: 'Share', + }), + description: i18n('kbn.discover.localMenu.shareSearchDescription', { + defaultMessage: 'Share Search', + }), + testId: 'shareTopNavButton', + run: async (menuItem, navController, anchorElement) => { + const sharingData = await this.getSharingData(); + showShareContextMenu({ + anchorElement, + allowEmbed: false, + allowShortUrl: uiCapabilities.discover.createShortUrl, + getUnhashableStates, + objectId: savedSearch.id, + objectType: 'search', + shareContextMenuExtensions, + sharingData: { + ...sharingData, + title: savedSearch.title, + }, + isDirty: $appStatus.dirty, + }); + } + }; + + const inspectSearch = { + key: 'inspect', + label: i18n('kbn.discover.localMenu.inspectTitle', { + defaultMessage: 'Inspect', + }), + description: i18n('kbn.discover.localMenu.openInspectorForSearchDescription', { + defaultMessage: 'Open Inspector for search', + }), + testId: 'openInspectorButton', + run() { + Inspector.open(inspectorAdapters, { + title: savedSearch.title + }); + } + }; + + return [ + newSearch, + ...uiCapabilities.discover.save ? [saveSearch] : [], + openSearch, + shareSearch, + inspectSearch, + ]; + }; + + $scope.topNavMenu = getTopNavLinks(); // the actual courier.SearchSource $scope.searchSource = savedSearch.searchSource; diff --git a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts index 28610ef01bd3..079fb1f72bf9 100644 --- a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts +++ b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts @@ -58,6 +58,7 @@ interface SearchEmbeddableConfig { onEmbeddableStateChanged: OnEmbeddableStateChanged; savedSearch: SavedSearch; editUrl: string; + editable: boolean; $rootScope: ng.IRootScopeService; $compile: ng.ICompileService; } @@ -80,6 +81,7 @@ export class SearchEmbeddable extends Embeddable { constructor({ onEmbeddableStateChanged, savedSearch, + editable, editUrl, $rootScope, $compile, @@ -87,6 +89,7 @@ export class SearchEmbeddable extends Embeddable { super({ title: savedSearch.title, editUrl, + editable, indexPatterns: _.compact([savedSearch.searchSource.getField('index')]), }); this.onEmbeddableStateChanged = onEmbeddableStateChanged; diff --git a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts index d869643f2f92..fbfa930fe145 100644 --- a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts +++ b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts @@ -18,7 +18,7 @@ */ import '../doc_table'; - +import { uiCapabilities } from 'ui/capabilities'; import { i18n } from '@kbn/i18n'; import { EmbeddableFactory } from 'ui/embeddable'; import { @@ -63,6 +63,7 @@ export class SearchEmbeddableFactory extends EmbeddableFactory { onEmbeddableStateChanged: OnEmbeddableStateChanged ) { const editUrl = this.getEditPath(id); + const editable = uiCapabilities.discover.save as boolean; // can't change this to be async / awayt, because an Anglular promise is expected to be returned. return this.searchLoader.get(id).then(savedObject => { @@ -70,6 +71,7 @@ export class SearchEmbeddableFactory extends EmbeddableFactory { onEmbeddableStateChanged, savedSearch: savedObject, editUrl, + editable, $rootScope: this.$rootScope, $compile: this.$compile, }); diff --git a/src/legacy/core_plugins/kibana/public/home/home_ng_wrapper.html b/src/legacy/core_plugins/kibana/public/home/home_ng_wrapper.html index 2a046a0549cb..645855766fab 100644 --- a/src/legacy/core_plugins/kibana/public/home/home_ng_wrapper.html +++ b/src/legacy/core_plugins/kibana/public/home/home_ng_wrapper.html @@ -1,4 +1,5 @@ diff --git a/src/legacy/core_plugins/kibana/public/kibana.js b/src/legacy/core_plugins/kibana/public/kibana.js index 68998f78a245..1bc91360eddb 100644 --- a/src/legacy/core_plugins/kibana/public/kibana.js +++ b/src/legacy/core_plugins/kibana/public/kibana.js @@ -65,6 +65,7 @@ import 'leaflet'; routes.enable(); + routes .otherwise({ redirectTo: `/${chrome.getInjected('kbnDefaultAppId', 'discover')}` diff --git a/src/legacy/core_plugins/kibana/public/management/app.html b/src/legacy/core_plugins/kibana/public/management/app.html index 2a3ecd62a895..11198c02960c 100644 --- a/src/legacy/core_plugins/kibana/public/management/app.html +++ b/src/legacy/core_plugins/kibana/public/management/app.html @@ -1,4 +1,4 @@

\ No newline at end of file + diff --git a/src/legacy/core_plugins/kibana/public/management/index.js b/src/legacy/core_plugins/kibana/public/management/index.js index 3001e0c0cc06..4f015ee28bf3 100644 --- a/src/legacy/core_plugins/kibana/public/management/index.js +++ b/src/legacy/core_plugins/kibana/public/management/index.js @@ -141,7 +141,7 @@ uiModules link: function ($scope) { timefilter.disableAutoRefreshSelector(); timefilter.disableTimeRangeSelector(); - $scope.sections = management.items.inOrder; + $scope.sections = management.visibleItems; $scope.section = management.getSection($scope.sectionName) || management; if ($scope.section) { @@ -152,7 +152,7 @@ uiModules updateSidebar($scope.sections, $scope.section.id); $scope.$on('$destroy', () => destroyReact(SIDENAV_ID)); - management.addListener(() => updateSidebar(management.items.inOrder, $scope.section.id)); + management.addListener(() => updateSidebar(management.visibleItems, $scope.section.id)); updateLandingPage($scope.$root.chrome.getKibanaVersion()); $scope.$on('$destroy', () => destroyReact(LANDING_ID)); @@ -166,7 +166,7 @@ uiModules return { restrict: 'E', link: function ($scope) { - $scope.sections = management.items.inOrder; + $scope.sections = management.visibleItems; $scope.kbnVersion = kbnVersion; } }; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_button/create_button.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_button/create_button.tsx index 9dd6588f4597..928212ba48bb 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_button/create_button.tsx +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_button/create_button.tsx @@ -33,6 +33,8 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { UICapabilities } from 'ui/capabilities'; +import { injectUICapabilities } from 'ui/capabilities/react'; interface State { isPopoverOpen: boolean; @@ -46,21 +48,26 @@ interface Props { isBeta?: boolean; onClick: () => void; }>; + uiCapabilities: UICapabilities; } -export class CreateButton extends Component { +class CreateButtonComponent extends Component { public state = { isPopoverOpen: false, }; public render() { - const { options, children } = this.props; + const { options, children, uiCapabilities } = this.props; const { isPopoverOpen } = this.state; if (!options || !options.length) { return null; } + if (!uiCapabilities.indexPatterns.createNew) { + return null; + } + if (options.length === 1) { return ( { ); }; } + +export const CreateButton = injectUICapabilities(CreateButtonComponent); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index.js index d071af0f15bc..321599100a6c 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index.js @@ -31,6 +31,7 @@ import { SavedObjectsClientProvider } from 'ui/saved_objects'; import { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory } from 'ui/registry/feature_catalogue'; import { i18n } from '@kbn/i18n'; import { I18nContext } from 'ui/i18n'; +import { UICapabilitiesProvider } from 'ui/capabilities/react'; import { EuiBadge } from '@elastic/eui'; import { getListBreadcrumbs } from './breadcrumbs'; @@ -51,11 +52,13 @@ export function updateIndexPatternList( render( - + + + , node, ); @@ -81,7 +84,8 @@ const indexPatternsResolutions = { // add a dependency to all of the subsection routes uiRoutes .defaults(/management\/kibana\/(index_patterns|index_pattern)/, { - resolve: indexPatternsResolutions + resolve: indexPatternsResolutions, + requireUICapability: 'management.kibana.index_patterns', }); uiRoutes diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index_pattern_table/index_pattern_table.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index_pattern_table/index_pattern_table.tsx index 15e0f9a22639..79ff3d8c624d 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index_pattern_table/index_pattern_table.tsx +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index_pattern_table/index_pattern_table.tsx @@ -85,7 +85,7 @@ export class IndexPatternTable extends React.Component { public render() { return ( - + {this.state.showFlyout && ( this.setState({ showFlyout: false })} /> )} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.js index fae371ed36a3..230e4ada7417 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.js @@ -27,7 +27,7 @@ import { uiModules } from 'ui/modules'; import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { ObjectsTable } from './components/objects_table'; -import { getInAppUrl } from './lib/get_in_app_url'; +import { canViewInApp, getInAppUrl } from './lib/in_app_url'; import { I18nContext } from 'ui/i18n'; import { getIndexBreadcrumbs } from './breadcrumbs'; @@ -40,6 +40,7 @@ function updateObjectsTable($scope, $injector, i18n) { const $http = $injector.get('$http'); const kbnUrl = $injector.get('kbnUrl'); const config = $injector.get('config'); + const uiCapabilites = chrome.getInjected('uiCapabilities'); const savedObjectsClient = Private(SavedObjectsClientProvider); const services = savedObjectManagementRegistry.all().map(obj => $injector.get(obj.service)); @@ -64,6 +65,7 @@ function updateObjectsTable($scope, $injector, i18n) { perPageConfig={config.get('savedObjects:perPage')} basePath={chrome.getBasePath()} newIndexPatternUrl={kbnUrl.eval('#/management/kibana/index_pattern')} + uiCapabilities={uiCapabilites} getEditUrl={(id, type) => { if (type === 'index-pattern' || type === 'indexPatterns') { return kbnUrl.eval(`#/management/kibana/index_patterns/${id}`); @@ -79,6 +81,9 @@ function updateObjectsTable($scope, $injector, i18n) { return kbnUrl.eval(`#/management/kibana/objects/${serviceName}/${id}`); }} + canGoInApp={(type) => { + return canViewInApp(uiCapabilites, type); + }} goInApp={(id, type) => { kbnUrl.change(getInAppUrl(id, type)); $scope.$apply(); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.html b/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.html index 5922160dae85..6efef7b48fa0 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.html +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.html @@ -1,4 +1,4 @@ - +
@@ -8,6 +8,15 @@ i18n-id="kbn.management.objects.view.editItemTitle" i18n-default-message="Edit {title}" i18n-values="{ title }" + ng-if="canEdit" + > + +

@@ -15,6 +24,7 @@
@@ -29,6 +39,8 @@ diff --git a/src/legacy/plugin_discovery/plugin_spec/plugin_spec.js b/src/legacy/plugin_discovery/plugin_spec/plugin_spec.js index d784afe504f1..9bf6bcd436d9 100644 --- a/src/legacy/plugin_discovery/plugin_spec/plugin_spec.js +++ b/src/legacy/plugin_discovery/plugin_spec/plugin_spec.js @@ -62,6 +62,7 @@ export class PluginSpec { deprecations, preInit, init, + postInit, isEnabled, } = options; @@ -81,6 +82,7 @@ export class PluginSpec { this._isEnabled = isEnabled; this._preInit = preInit; this._init = init; + this._postInit = postInit; if (!this.getId()) { throw createInvalidPluginError(this, 'Unable to determine plugin id'); @@ -176,6 +178,10 @@ export class PluginSpec { return this._init; } + getPostInitHandler() { + return this._postInit; + } + getConfigPrefix() { return this._configPrefix || this.getId(); } diff --git a/src/legacy/server/plugins/initialize_mixin.js b/src/legacy/server/plugins/initialize_mixin.js index 2fb08e83efd0..9cc317f002c5 100644 --- a/src/legacy/server/plugins/initialize_mixin.js +++ b/src/legacy/server/plugins/initialize_mixin.js @@ -43,4 +43,5 @@ export async function initializeMixin(kbnServer, server, config) { await callHookOnPlugins('preInit'); await callHookOnPlugins('init'); + await callHookOnPlugins('postInit'); } diff --git a/src/legacy/server/plugins/lib/plugin.js b/src/legacy/server/plugins/lib/plugin.js index 46dd047a657c..98bbbca35ece 100644 --- a/src/legacy/server/plugins/lib/plugin.js +++ b/src/legacy/server/plugins/lib/plugin.js @@ -43,12 +43,14 @@ export class Plugin { this.requiredIds = spec.getRequiredPluginIds() || []; this.externalPreInit = spec.getPreInitHandler(); this.externalInit = spec.getInitHandler(); + this.externalPostInit = spec.getPostInitHandler(); this.enabled = spec.isEnabled(kbnServer.config); this.configPrefix = spec.getConfigPrefix(); this.publicDir = spec.getPublicDir(); this.preInit = once(this.preInit); this.init = once(this.init); + this.postInit = once(this.postInit); } async preInit() { @@ -96,6 +98,12 @@ export class Plugin { } } + async postInit() { + if (this.externalPostInit) { + return await this.externalPostInit(this.kbnServer.server); + } + } + getServer() { return this._server; } diff --git a/src/legacy/ui/public/capabilities/index.ts b/src/legacy/ui/public/capabilities/index.ts new file mode 100644 index 000000000000..ca798d2d70ea --- /dev/null +++ b/src/legacy/ui/public/capabilities/index.ts @@ -0,0 +1,31 @@ +/* + * 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 { Capabilities as UICapabilities, CapabilitiesSetup } from '../../../../core/public'; + +export { Capabilities as UICapabilities } from '../../../../core/public'; +export let uiCapabilities: UICapabilities = null!; + +export function __newPlatformInit__(capabililitiesService: CapabilitiesSetup) { + if (uiCapabilities) { + throw new Error('ui/capabilities already initialized with new platform apis'); + } + + uiCapabilities = capabililitiesService.getCapabilities(); +} diff --git a/src/legacy/ui/public/capabilities/react/index.ts b/src/legacy/ui/public/capabilities/react/index.ts new file mode 100644 index 000000000000..78d95a260329 --- /dev/null +++ b/src/legacy/ui/public/capabilities/react/index.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +export { UICapabilitiesProvider } from './ui_capabilities_provider'; +export { injectUICapabilities } from './inject_ui_capabilities'; diff --git a/src/legacy/ui/public/capabilities/react/inject_ui_capabilities.test.tsx b/src/legacy/ui/public/capabilities/react/inject_ui_capabilities.test.tsx new file mode 100644 index 000000000000..f89112fd8242 --- /dev/null +++ b/src/legacy/ui/public/capabilities/react/inject_ui_capabilities.test.tsx @@ -0,0 +1,113 @@ +/* + * 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. + */ + +jest.mock('ui/capabilities', () => ({ + uiCapabilities: { + uiCapability1: true, + uiCapability2: { + nestedProp: 'nestedValue', + }, + }, +})); + +import { mount } from 'enzyme'; +import React from 'react'; +import { UICapabilities } from '..'; +import { injectUICapabilities } from './inject_ui_capabilities'; +import { UICapabilitiesProvider } from './ui_capabilities_provider'; + +describe('injectUICapabilities', () => { + it('provides UICapabilities to SFCs', () => { + interface SFCProps { + uiCapabilities: UICapabilities; + } + + const MySFC = injectUICapabilities(({ uiCapabilities }: SFCProps) => { + return {uiCapabilities.uiCapability2.nestedProp}; + }); + + const wrapper = mount( + + + + ); + + expect(wrapper).toMatchInlineSnapshot(` + + + + + nestedValue + + + + +`); + }); + + it('provides UICapabilities to class components', () => { + interface ClassProps { + uiCapabilities: UICapabilities; + } + + class MyClassComponent extends React.Component { + public render() { + return {this.props.uiCapabilities.uiCapability2.nestedProp}; + } + } + + const WrappedComponent = injectUICapabilities(MyClassComponent); + + const wrapper = mount( + + + + ); + + expect(wrapper).toMatchInlineSnapshot(` + + + + + nestedValue + + + + +`); + }); +}); diff --git a/src/legacy/ui/public/capabilities/react/inject_ui_capabilities.tsx b/src/legacy/ui/public/capabilities/react/inject_ui_capabilities.tsx new file mode 100644 index 000000000000..5f12c2011990 --- /dev/null +++ b/src/legacy/ui/public/capabilities/react/inject_ui_capabilities.tsx @@ -0,0 +1,53 @@ +/* + * 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 React, { Component, ComponentClass, ComponentType } from 'react'; +import { UICapabilities } from '..'; +import { UICapabilitiesContext } from './ui_capabilities_context'; + +function getDisplayName(component: ComponentType) { + return component.displayName || component.name || 'Component'; +} + +interface InjectedProps { + uiCapabilities: UICapabilities; +} + +export function injectUICapabilities

( + WrappedComponent: ComponentType

+): ComponentClass>> & { + WrappedComponent: ComponentType

; +} { + class InjectUICapabilities extends Component { + public static displayName = `InjectUICapabilities(${getDisplayName(WrappedComponent)})`; + + public static WrappedComponent: ComponentType

= WrappedComponent; + + public static contextType = UICapabilitiesContext; + + constructor(props: any, context: any) { + super(props, context); + } + + public render() { + return ; + } + } + return InjectUICapabilities; +} diff --git a/src/legacy/ui/public/capabilities/react/legacy/index.ts b/src/legacy/ui/public/capabilities/react/legacy/index.ts new file mode 100644 index 000000000000..78d95a260329 --- /dev/null +++ b/src/legacy/ui/public/capabilities/react/legacy/index.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +export { UICapabilitiesProvider } from './ui_capabilities_provider'; +export { injectUICapabilities } from './inject_ui_capabilities'; diff --git a/src/legacy/ui/public/capabilities/react/legacy/inject_ui_capabilities.test.tsx b/src/legacy/ui/public/capabilities/react/legacy/inject_ui_capabilities.test.tsx new file mode 100644 index 000000000000..1ee8ba72c6cc --- /dev/null +++ b/src/legacy/ui/public/capabilities/react/legacy/inject_ui_capabilities.test.tsx @@ -0,0 +1,113 @@ +/* + * 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. + */ + +jest.mock('ui/capabilities', () => ({ + uiCapabilities: { + uiCapability1: true, + uiCapability2: { + nestedProp: 'nestedValue', + }, + }, +})); + +import { mount } from 'enzyme'; +import React from 'react'; +import { UICapabilities } from '../..'; +import { injectUICapabilities } from './inject_ui_capabilities'; +import { UICapabilitiesProvider } from './ui_capabilities_provider'; + +describe('injectUICapabilities', () => { + it('provides UICapabilities to SFCs', () => { + interface SFCProps { + uiCapabilities: UICapabilities; + } + + const MySFC = injectUICapabilities(({ uiCapabilities }: SFCProps) => { + return {uiCapabilities.uiCapability2.nestedProp}; + }); + + const wrapper = mount( + + + + ); + + expect(wrapper).toMatchInlineSnapshot(` + + + + + nestedValue + + + + +`); + }); + + it('provides UICapabilities to class components', () => { + interface ClassProps { + uiCapabilities: UICapabilities; + } + + class MyClassComponent extends React.Component { + public render() { + return {this.props.uiCapabilities.uiCapability2.nestedProp}; + } + } + + const WrappedComponent = injectUICapabilities(MyClassComponent); + + const wrapper = mount( + + + + ); + + expect(wrapper).toMatchInlineSnapshot(` + + + + + nestedValue + + + + +`); + }); +}); diff --git a/src/legacy/ui/public/capabilities/react/legacy/inject_ui_capabilities.tsx b/src/legacy/ui/public/capabilities/react/legacy/inject_ui_capabilities.tsx new file mode 100644 index 000000000000..025332680972 --- /dev/null +++ b/src/legacy/ui/public/capabilities/react/legacy/inject_ui_capabilities.tsx @@ -0,0 +1,57 @@ +/* + * 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 PropTypes from 'prop-types'; +import React, { Component, ComponentClass, ComponentType } from 'react'; +import { UICapabilities } from '../..'; + +function getDisplayName(component: ComponentType) { + return component.displayName || component.name || 'Component'; +} + +interface InjectedProps { + uiCapabilities: UICapabilities; +} + +export function injectUICapabilities

( + WrappedComponent: ComponentType

+): ComponentClass>> & { + WrappedComponent: ComponentType

; +} { + class InjectUICapabilities extends Component { + public static displayName = `InjectUICapabilities(${getDisplayName(WrappedComponent)})`; + + public static WrappedComponent: ComponentType

= WrappedComponent; + + public static contextTypes = { + uiCapabilities: PropTypes.object.isRequired, + }; + + constructor(props: any, context: any) { + super(props, context); + } + + public render() { + return ( + + ); + } + } + return InjectUICapabilities; +} diff --git a/src/legacy/ui/public/capabilities/react/legacy/ui_capabilities_provider.tsx b/src/legacy/ui/public/capabilities/react/legacy/ui_capabilities_provider.tsx new file mode 100644 index 000000000000..856665acd65b --- /dev/null +++ b/src/legacy/ui/public/capabilities/react/legacy/ui_capabilities_provider.tsx @@ -0,0 +1,48 @@ +/* + * 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 PropTypes from 'prop-types'; +import React, { ReactNode } from 'react'; +import { uiCapabilities, UICapabilities } from '../..'; + +interface Props { + children: ReactNode; +} + +interface ProviderContext { + uiCapabilities: UICapabilities; +} + +export class UICapabilitiesProvider extends React.Component { + public static displayName: string = 'UICapabilitiesProvider'; + + public static childContextTypes = { + uiCapabilities: PropTypes.object.isRequired, + }; + + public getChildContext(): ProviderContext { + return { + uiCapabilities, + }; + } + + public render() { + return React.Children.only(this.props.children); + } +} diff --git a/src/legacy/ui/public/capabilities/react/ui_capabilities_context.ts b/src/legacy/ui/public/capabilities/react/ui_capabilities_context.ts new file mode 100644 index 000000000000..ed299662899b --- /dev/null +++ b/src/legacy/ui/public/capabilities/react/ui_capabilities_context.ts @@ -0,0 +1,27 @@ +/* + * 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 React from 'react'; +import { UICapabilities } from '..'; + +export const UICapabilitiesContext = React.createContext({ + navLinks: {}, + catalogue: {}, + management: {}, +}); diff --git a/src/legacy/ui/public/capabilities/react/ui_capabilities_provider.tsx b/src/legacy/ui/public/capabilities/react/ui_capabilities_provider.tsx new file mode 100644 index 000000000000..42c10004b766 --- /dev/null +++ b/src/legacy/ui/public/capabilities/react/ui_capabilities_provider.tsx @@ -0,0 +1,38 @@ +/* + * 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 React, { ReactNode } from 'react'; +import { uiCapabilities } from '..'; +import { UICapabilitiesContext } from './ui_capabilities_context'; + +interface Props { + children: ReactNode; +} + +export class UICapabilitiesProvider extends React.Component { + public static displayName: string = 'UICapabilitiesProvider'; + + public render() { + return ( + + {this.props.children} + + ); + } +} diff --git a/src/legacy/ui/public/capabilities/route_setup.ts b/src/legacy/ui/public/capabilities/route_setup.ts new file mode 100644 index 000000000000..c7817b8cc574 --- /dev/null +++ b/src/legacy/ui/public/capabilities/route_setup.ts @@ -0,0 +1,38 @@ +/* + * 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 { get } from 'lodash'; +import chrome from 'ui/chrome'; +import uiRoutes from 'ui/routes'; +import { UICapabilities } from '.'; + +uiRoutes.addSetupWork( + (uiCapabilities: UICapabilities, kbnBaseUrl: string, $route: any, kbnUrl: any) => { + const route = get($route, 'current.$$route') as any; + if (!route.requireUICapability) { + return; + } + + if (!get(uiCapabilities, route.requireUICapability)) { + const url = chrome.addBasePath(`${kbnBaseUrl}#/home`); + kbnUrl.redirect(url); + throw uiRoutes.WAIT_FOR_URL_CHANGE_TOKEN; + } + } +); diff --git a/src/legacy/ui/public/chrome/directives/header_global_nav/components/header.tsx b/src/legacy/ui/public/chrome/directives/header_global_nav/components/header.tsx index 98e176fdc704..41d50e7b8be2 100644 --- a/src/legacy/ui/public/chrome/directives/header_global_nav/components/header.tsx +++ b/src/legacy/ui/public/chrome/directives/header_global_nav/components/header.tsx @@ -52,6 +52,7 @@ import { import { i18n } from '@kbn/i18n'; import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; +import { UICapabilities } from 'ui/capabilities'; import chrome, { NavLink } from 'ui/chrome'; import { HelpExtension } from 'ui/chrome'; import { RecentlyAccessedHistoryItem } from 'ui/persisted_log'; @@ -76,6 +77,7 @@ interface Props { helpExtension$: Rx.Observable; navControls: ChromeHeaderNavControlsRegistry; intl: InjectedIntl; + uiCapabilities: UICapabilities; } // Providing a buffer between the limit and the cut off index @@ -212,7 +214,15 @@ class HeaderUI extends Component { } public render() { - const { appTitle, breadcrumbs$, isVisible, navControls, helpExtension$, intl } = this.props; + const { + appTitle, + breadcrumbs$, + isVisible, + navControls, + helpExtension$, + intl, + uiCapabilities, + } = this.props; const { navLinks, recentlyAccessed } = this.state; if (!isVisible) { @@ -223,7 +233,7 @@ class HeaderUI extends Component { const rightNavControls = navControls.bySide[NavControlSide.Right]; let navLinksArray = navLinks.map(navLink => - navLink.hidden + navLink.hidden || !uiCapabilities.navLinks[navLink.id] ? null : { key: navLink.id, diff --git a/src/legacy/ui/public/chrome/directives/header_global_nav/header_global_nav.js b/src/legacy/ui/public/chrome/directives/header_global_nav/header_global_nav.js index 66b34b38df6c..062ebc258b48 100644 --- a/src/legacy/ui/public/chrome/directives/header_global_nav/header_global_nav.js +++ b/src/legacy/ui/public/chrome/directives/header_global_nav/header_global_nav.js @@ -25,7 +25,7 @@ import { chromeHeaderNavControlsRegistry } from 'ui/registry/chrome_header_nav_c const module = uiModules.get('kibana'); -module.directive('headerGlobalNav', (reactDirective, chrome, Private) => { +module.directive('headerGlobalNav', (reactDirective, chrome, Private, uiCapabilities) => { const { recentlyAccessed } = require('ui/persisted_log'); const navControls = Private(chromeHeaderNavControlsRegistry); const homeHref = chrome.addBasePath('/app/kibana#/home'); @@ -44,6 +44,7 @@ module.directive('headerGlobalNav', (reactDirective, chrome, Private) => { recentlyAccessed$: recentlyAccessed.get$(), forceAppSwitcherNavigation$: chrome.getForceAppSwitcherNavigation$(), navControls, - homeHref + homeHref, + uiCapabilities, }); }); diff --git a/src/legacy/ui/public/embeddable/embeddable.ts b/src/legacy/ui/public/embeddable/embeddable.ts index a6ce01cb2c05..f70ee5a114bb 100644 --- a/src/legacy/ui/public/embeddable/embeddable.ts +++ b/src/legacy/ui/public/embeddable/embeddable.ts @@ -39,6 +39,11 @@ export interface EmbeddableMetadata { * offer for editing directly on the dashboard. */ editUrl?: string; + + /** + * A flag indicating if this embeddable can be edited. + */ + editable?: boolean; } export abstract class Embeddable { diff --git a/src/legacy/ui/public/management/components/sidebar_nav.test.ts b/src/legacy/ui/public/management/components/sidebar_nav.test.ts index 181c3c76a6c1..e02cc7d2901b 100644 --- a/src/legacy/ui/public/management/components/sidebar_nav.test.ts +++ b/src/legacy/ui/public/management/components/sidebar_nav.test.ts @@ -36,25 +36,25 @@ const visibleItem = { display: 'item', id: 'item', ...activeProps }; const notVisibleSection = { display: 'Not visible', id: 'not-visible', - items: toIndexedArray([visibleItem]), + visibleItems: toIndexedArray([visibleItem]), ...notVisibleProps, }; const disabledSection = { display: 'Disabled', id: 'disabled', - items: toIndexedArray([visibleItem]), + visibleItems: toIndexedArray([visibleItem]), ...disabledProps, }; const noItemsSection = { display: 'No items', id: 'no-items', - items: toIndexedArray([]), + visibleItems: toIndexedArray([]), ...activeProps, }; const noActiveItemsSection = { display: 'No active items', id: 'no-active-items', - items: toIndexedArray([ + visibleItems: toIndexedArray([ { display: 'disabled', id: 'disabled', ...disabledProps }, { display: 'notVisible', id: 'notVisible', ...notVisibleProps }, ]), @@ -63,7 +63,7 @@ const noActiveItemsSection = { const activeSection = { display: 'activeSection', id: 'activeSection', - items: toIndexedArray([visibleItem]), + visibleItems: toIndexedArray([visibleItem]), ...activeProps, }; diff --git a/src/legacy/ui/public/management/components/sidebar_nav.tsx b/src/legacy/ui/public/management/components/sidebar_nav.tsx index 373fdadc2ae4..ef232c7ef7ed 100644 --- a/src/legacy/ui/public/management/components/sidebar_nav.tsx +++ b/src/legacy/ui/public/management/components/sidebar_nav.tsx @@ -31,7 +31,7 @@ interface Subsection { icon?: IconType; } interface Section extends Subsection { - items: IndexedArray; + visibleItems: IndexedArray; } const sectionVisible = (section: Subsection) => !section.disabled && section.visible; @@ -47,9 +47,9 @@ const sectionToNav = (selectedId: string) => ({ display, id, url, icon }: Subsec export const sideNavItems = (sections: Section[], selectedId: string) => sections .filter(sectionVisible) - .filter(section => section.items.filter(sectionVisible).length) + .filter(section => section.visibleItems.filter(sectionVisible).length) .map(section => ({ - items: section.items.inOrder.filter(sectionVisible).map(sectionToNav(selectedId)), + items: section.visibleItems.filter(sectionVisible).map(sectionToNav(selectedId)), ...sectionToNav(selectedId)(section), })); diff --git a/src/legacy/ui/public/management/section.js b/src/legacy/ui/public/management/section.js index 72b44d978fe3..76fe4f024f39 100644 --- a/src/legacy/ui/public/management/section.js +++ b/src/legacy/ui/public/management/section.js @@ -19,6 +19,7 @@ import { assign } from 'lodash'; import { IndexedArray } from '../indexed_array'; +import { uiCapabilities } from '../capabilities'; const listeners = []; @@ -50,10 +51,16 @@ export class ManagementSection { this.url = ''; assign(this, options); + } get visibleItems() { - return this.items.inOrder.filter(item => item.visible); + return this.items.inOrder.filter(item => { + const capabilityManagementSection = uiCapabilities.management[this.id]; + const itemCapability = capabilityManagementSection ? capabilityManagementSection[item.id] : null; + + return item.visible && itemCapability !== false; + }); } /** diff --git a/src/legacy/ui/public/management/__tests__/section.js b/src/legacy/ui/public/management/section.test.js similarity index 70% rename from src/legacy/ui/public/management/__tests__/section.js rename to src/legacy/ui/public/management/section.test.js index cc49d346213d..eff92fb30789 100644 --- a/src/legacy/ui/public/management/__tests__/section.js +++ b/src/legacy/ui/public/management/section.test.js @@ -16,53 +16,62 @@ * specific language governing permissions and limitations * under the License. */ +jest.mock('ui/capabilities', () => ({ + uiCapabilities: { + navLinks: {}, + management: { + kibana: { + sampleFeature1: true, + sampleFeature2: false, + } + } + } +})); -import expect from '@kbn/expect'; - -import { ManagementSection } from '../section'; -import { IndexedArray } from '../../indexed_array'; +import { ManagementSection } from './section'; +import { IndexedArray } from '../indexed_array'; describe('ManagementSection', () => { describe('constructor', () => { it('defaults display to id', () => { const section = new ManagementSection('kibana'); - expect(section.display).to.be('kibana'); + expect(section.display).toBe('kibana'); }); it('defaults visible to true', () => { const section = new ManagementSection('kibana'); - expect(section.visible).to.be(true); + expect(section.visible).toBe(true); }); it('defaults disabled to false', () => { const section = new ManagementSection('kibana'); - expect(section.disabled).to.be(false); + expect(section.disabled).toBe(false); }); it('defaults tooltip to empty string', () => { const section = new ManagementSection('kibana'); - expect(section.tooltip).to.be(''); + expect(section.tooltip).toBe(''); }); it('defaults url to empty string', () => { const section = new ManagementSection('kibana'); - expect(section.url).to.be(''); + expect(section.url).toBe(''); }); it('exposes items', () => { const section = new ManagementSection('kibana'); - expect(section.items).to.be.empty(); + expect(section.items).toHaveLength(0); }); it('exposes visibleItems', () => { const section = new ManagementSection('kibana'); - expect(section.visibleItems).to.be.empty(); + expect(section.visibleItems).toHaveLength(0); }); it('assigns all options', () => { const section = new ManagementSection('kibana', { description: 'test', url: 'foobar' }); - expect(section.description).to.be('test'); - expect(section.url).to.be('foobar'); + expect(section.description).toBe('test'); + expect(section.url).toBe('foobar'); }); }); @@ -74,19 +83,19 @@ describe('ManagementSection', () => { }); it('returns a ManagementSection', () => { - expect(section.register('about')).to.be.a(ManagementSection); + expect(section.register('about')).toBeInstanceOf(ManagementSection); }); it('provides a reference to the parent', () => { - expect(section.register('about').parent).to.be(section); + expect(section.register('about').parent).toBe(section); }); it('adds item', function () { section.register('about', { description: 'test' }); - expect(section.items).to.have.length(1); - expect(section.items[0]).to.be.a(ManagementSection); - expect(section.items[0].id).to.be('about'); + expect(section.items).toHaveLength(1); + expect(section.items[0]).toBeInstanceOf(ManagementSection); + expect(section.items[0].id).toBe('about'); }); it('can only register a section once', () => { @@ -99,7 +108,7 @@ describe('ManagementSection', () => { threwException = e.message.indexOf('is already registered') > -1; } - expect(threwException).to.be(true); + expect(threwException).toBe(true); }); it('calls listener when item added', () => { @@ -108,7 +117,7 @@ describe('ManagementSection', () => { section.addListener(listenerFn); section.register('about'); - expect(listerCalled).to.be(true); + expect(listerCalled).toBe(true); }); }); @@ -122,13 +131,13 @@ describe('ManagementSection', () => { it ('deregisters an existing section', () => { section.deregister('about'); - expect(section.items).to.have.length(0); + expect(section.items).toHaveLength(0); }); it ('allows deregistering a section more than once', () => { section.deregister('about'); section.deregister('about'); - expect(section.items).to.have.length(0); + expect(section.items).toHaveLength(0); }); it('calls listener when item added', () => { @@ -137,7 +146,7 @@ describe('ManagementSection', () => { section.addListener(listenerFn); section.deregister('about'); - expect(listerCalled).to.be(true); + expect(listerCalled).toBe(true); }); }); @@ -150,21 +159,21 @@ describe('ManagementSection', () => { }); it('returns registered section', () => { - expect(section.getSection('about')).to.be.a(ManagementSection); + expect(section.getSection('about')).toBeInstanceOf(ManagementSection); }); it('returns undefined if un-registered', () => { - expect(section.getSection('unknown')).to.be(undefined); + expect(section.getSection('unknown')).not.toBeDefined(); }); it('returns sub-sections specified via a /-separated path', () => { section.getSection('about').register('time'); - expect(section.getSection('about/time')).to.be.a(ManagementSection); - expect(section.getSection('about/time')).to.be(section.getSection('about').getSection('time')); + expect(section.getSection('about/time')).toBeInstanceOf(ManagementSection); + expect(section.getSection('about/time')).toBe(section.getSection('about').getSection('time')); }); it('returns undefined if a sub-section along a /-separated path does not exist', () => { - expect(section.getSection('about/damn/time')).to.be(undefined); + expect(section.getSection('about/damn/time')).toBe(undefined); }); }); @@ -180,19 +189,19 @@ describe('ManagementSection', () => { }); it('is an indexed array', () => { - expect(section.items).to.be.a(IndexedArray); + expect(section.items).toBeInstanceOf(IndexedArray); }); it('is indexed on id', () => { const keys = Object.keys(section.items.byId).sort(); - expect(section.items.byId).to.be.an('object'); + expect(section.items.byId).toBeInstanceOf(Object); - expect(keys).to.eql(['one', 'three', 'two']); + expect(keys).toEqual(['one', 'three', 'two']); }); it('can be ordered', () => { const ids = section.items.inOrder.map((i) => { return i.id; }); - expect(ids).to.eql(['one', 'two', 'three']); + expect(ids).toEqual(['one', 'two', 'three']); }); }); @@ -205,13 +214,13 @@ describe('ManagementSection', () => { it('hide sets visible to false', () => { section.hide(); - expect(section.visible).to.be(false); + expect(section.visible).toBe(false); }); it('show sets visible to true', () => { section.hide(); section.show(); - expect(section.visible).to.be(true); + expect(section.visible).toBe(true); }); }); @@ -224,12 +233,12 @@ describe('ManagementSection', () => { it('disable sets disabled to true', () => { section.disable(); - expect(section.disabled).to.be(true); + expect(section.disabled).toBe(true); }); it('enable sets disabled to false', () => { section.enable(); - expect(section.disabled).to.be(false); + expect(section.disabled).toBe(false); }); }); @@ -246,15 +255,20 @@ describe('ManagementSection', () => { it('maintains the order', () => { const ids = section.visibleItems.map((i) => { return i.id; }); - expect(ids).to.eql(['one', 'two', 'three']); + expect(ids).toEqual(['one', 'two', 'three']); }); it('does not include hidden items', () => { section.getSection('two').hide(); const ids = section.visibleItems.map((i) => { return i.id; }); - expect(ids).to.eql(['one', 'three']); + expect(ids).toEqual(['one', 'three']); }); + it('does not include visible items hidden via uiCapabilities', () => { + section.register('sampleFeature2', { order: 4, visible: true }); + const ids = section.visibleItems.map((i) => { return i.id; }); + expect(ids).toEqual(['one', 'two', 'three']); + }); }); }); diff --git a/src/legacy/ui/public/registry/feature_catalogue.js b/src/legacy/ui/public/registry/feature_catalogue.js index af8a68d69a7d..e9b8742f94b0 100644 --- a/src/legacy/ui/public/registry/feature_catalogue.js +++ b/src/legacy/ui/public/registry/feature_catalogue.js @@ -18,13 +18,17 @@ */ import { uiRegistry } from './_registry'; +import { uiCapabilities } from '../capabilities'; export const FeatureCatalogueRegistryProvider = uiRegistry({ name: 'featureCatalogue', index: ['id'], group: ['category'], order: ['title'], - filter: featureCatalogItem => Object.keys(featureCatalogItem).length > 0 + filter: featureCatalogItem => { + const isDisabledViaCapabilities = uiCapabilities.catalogue[featureCatalogItem.id] === false; + return !isDisabledViaCapabilities && Object.keys(featureCatalogItem).length > 0; + } }); export const FeatureCatalogueCategory = { diff --git a/src/legacy/ui/public/registry/feature_catalogue.test.js b/src/legacy/ui/public/registry/feature_catalogue.test.js new file mode 100644 index 000000000000..bf97fbd93c15 --- /dev/null +++ b/src/legacy/ui/public/registry/feature_catalogue.test.js @@ -0,0 +1,97 @@ +/* + * 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. + */ +jest.mock('ui/capabilities', () => ({ + uiCapabilities: { + navLinks: {}, + management: {}, + catalogue: { + item1: true, + item2: false, + item3: true, + }, + } +})); +import { FeatureCatalogueCategory, FeatureCatalogueRegistryProvider } from './feature_catalogue'; + +describe('FeatureCatalogueRegistryProvider', () => { + + beforeAll(() => { + FeatureCatalogueRegistryProvider.register(() => { + return { + id: 'item1', + title: 'foo', + description: 'this is foo', + icon: 'savedObjectsApp', + path: '/app/kibana#/management/kibana/objects', + showOnHomePage: true, + category: FeatureCatalogueCategory.ADMIN, + }; + }); + + FeatureCatalogueRegistryProvider.register(() => { + return { + id: 'item2', + title: 'bar', + description: 'this is bar', + icon: 'savedObjectsApp', + path: '/app/kibana#/management/kibana/objects', + showOnHomePage: true, + category: FeatureCatalogueCategory.ADMIN, + }; + }); + + // intentionally not listed in uiCapabilities.catalogue above + FeatureCatalogueRegistryProvider.register(() => { + return { + id: 'item4', + title: 'secret', + description: 'this is a secret', + icon: 'savedObjectsApp', + path: '/app/kibana#/management/kibana/objects', + showOnHomePage: true, + category: FeatureCatalogueCategory.ADMIN, + }; + }); + }); + + it('should not return items hidden by uiCapabilities', () => { + const mockPrivate = entityFn => entityFn(); + const mockInjector = () => null; + + // eslint-disable-next-line new-cap + const foo = FeatureCatalogueRegistryProvider(mockPrivate, mockInjector).inTitleOrder; + expect(foo).toEqual([{ + id: 'item1', + title: 'foo', + description: 'this is foo', + icon: 'savedObjectsApp', + path: '/app/kibana#/management/kibana/objects', + showOnHomePage: true, + category: FeatureCatalogueCategory.ADMIN, + }, { + id: 'item4', + title: 'secret', + description: 'this is a secret', + icon: 'savedObjectsApp', + path: '/app/kibana#/management/kibana/objects', + showOnHomePage: true, + category: FeatureCatalogueCategory.ADMIN, + }]); + }); +}); diff --git a/src/legacy/ui/public/routes/route_manager.d.ts b/src/legacy/ui/public/routes/route_manager.d.ts index 10bdf9100182..3471d7e95486 100644 --- a/src/legacy/ui/public/routes/route_manager.d.ts +++ b/src/legacy/ui/public/routes/route_manager.d.ts @@ -30,9 +30,11 @@ interface RouteConfiguration { resolve?: object; template?: string; k7Breadcrumbs?: (...args: any[]) => ChromeBreadcrumb[]; + requireUICapability?: string; } interface RouteManager { + addSetupWork(cb: (...args: any[]) => void): void; when(path: string, routeConfiguration: RouteConfiguration): RouteManager; otherwise(routeConfiguration: RouteConfiguration): RouteManager; defaults(path: string | RegExp, defaults: RouteConfiguration): RouteManager; diff --git a/src/legacy/ui/public/routes/routes.d.ts b/src/legacy/ui/public/routes/routes.d.ts index 1a0a89612bf1..d48230e9d56f 100644 --- a/src/legacy/ui/public/routes/routes.d.ts +++ b/src/legacy/ui/public/routes/routes.d.ts @@ -20,6 +20,7 @@ import RouteManager from 'ui/routes/route_manager'; interface DefaultRouteManager extends RouteManager { + WAIT_FOR_URL_CHANGE_TOKEN: string; enable(): void; } diff --git a/src/legacy/ui/public/share/components/__snapshots__/url_panel_content.test.js.snap b/src/legacy/ui/public/share/components/__snapshots__/url_panel_content.test.js.snap index 3cdc655743ed..068a7246e714 100644 --- a/src/legacy/ui/public/share/components/__snapshots__/url_panel_content.test.js.snap +++ b/src/legacy/ui/public/share/components/__snapshots__/url_panel_content.test.js.snap @@ -110,6 +110,7 @@ exports[`render 1`] = ` /> `; + +exports[`should hide short url section when allowShortUrl is false 1`] = ` + + + } + labelType="label" + > + + + + + + + } + position="bottom" + type="questionInCircle" + /> + + , + }, + Object { + "data-test-subj": "exportAsSavedObject", + "disabled": false, + "id": "savedObject", + "label": + + + + + + } + position="bottom" + type="questionInCircle" + /> + + , + }, + ] + } + /> + + + + + +`; diff --git a/src/legacy/ui/public/share/components/share_context_menu.tsx b/src/legacy/ui/public/share/components/share_context_menu.tsx index b51cb5a04940..5d5c80f10e1d 100644 --- a/src/legacy/ui/public/share/components/share_context_menu.tsx +++ b/src/legacy/ui/public/share/components/share_context_menu.tsx @@ -28,6 +28,7 @@ import { UrlPanelContent } from './url_panel_content'; interface Props { allowEmbed: boolean; + allowShortUrl: boolean; objectId?: string; objectType: string; getUnhashableStates: () => object[]; @@ -63,6 +64,7 @@ class ShareContextMenuUI extends Component { }), content: ( { }), content: ( { const component = shallowWithIntl( {}} />); @@ -36,6 +37,17 @@ test('render', () => { test('should enable saved object export option when objectId is provided', () => { const component = shallowWithIntl( {}} + />); + expect(component).toMatchSnapshot(); +}); + +test('should hide short url section when allowShortUrl is false', () => { + const component = shallowWithIntl( {}} diff --git a/src/legacy/ui/public/share/components/url_panel_content.tsx b/src/legacy/ui/public/share/components/url_panel_content.tsx index c579def221ef..07c717379780 100644 --- a/src/legacy/ui/public/share/components/url_panel_content.tsx +++ b/src/legacy/ui/public/share/components/url_panel_content.tsx @@ -42,6 +42,7 @@ import { shortenUrl } from '../lib/url_shortener'; const FixedEuiIconTip = EuiIconTip as React.SFC; interface Props { + allowShortUrl: boolean; isEmbedded?: boolean; objectId?: string; objectType: string; @@ -354,7 +355,10 @@ class UrlPanelContentUI extends Component { }; private renderShortUrlSwitch = () => { - if (this.state.exportUrlAs === ExportUrlAsType.EXPORT_URL_AS_SAVED_OBJECT) { + if ( + this.state.exportUrlAs === ExportUrlAsType.EXPORT_URL_AS_SAVED_OBJECT || + !this.props.allowShortUrl + ) { return; } const shortUrlLabel = ( @@ -386,7 +390,7 @@ class UrlPanelContentUI extends Component { ); return ( - + {this.renderWithIconTip(switchComponent, tipContent)} ); diff --git a/src/legacy/ui/public/share/show_share_context_menu.tsx b/src/legacy/ui/public/share/show_share_context_menu.tsx index dcc685b0dbdf..1b3da0c6dc06 100644 --- a/src/legacy/ui/public/share/show_share_context_menu.tsx +++ b/src/legacy/ui/public/share/show_share_context_menu.tsx @@ -37,6 +37,7 @@ const onClose = () => { interface ShowProps { anchorElement: any; allowEmbed: boolean; + allowShortUrl: boolean; getUnhashableStates: () => object[]; objectId?: string; objectType: string; @@ -48,6 +49,7 @@ interface ShowProps { export function showShareContextMenu({ anchorElement, allowEmbed, + allowShortUrl, getUnhashableStates, objectId, objectType, @@ -76,6 +78,7 @@ export function showShareContextMenu({ > ({ + ...acc, + [navLinkSpec._id]: true + }), {}) + } + }; + } + let defaultInjectedVars = {}; kbnServer.afterPluginsInit(() => { const { defaultInjectedVarProviders = [] } = kbnServer.uiExports; @@ -48,7 +61,7 @@ export function uiRenderMixin(kbnServer, server, config) { allDefaults, fn(kbnServer.server, pluginSpec.readConfigValue(kbnServer.config, [])) ) - ), {}); + ), getInitialDefaultInjectedVars()); }); // render all views from ./views diff --git a/test/api_integration/apis/management/saved_objects/relationships.js b/test/api_integration/apis/management/saved_objects/relationships.js index 2a866245b1aa..d6e8030352ee 100644 --- a/test/api_integration/apis/management/saved_objects/relationships.js +++ b/test/api_integration/apis/management/saved_objects/relationships.js @@ -43,11 +43,15 @@ export default function ({ getService }) { visualization: GENERIC_RESPONSE_SCHEMA, 'index-pattern': GENERIC_RESPONSE_SCHEMA, }); + const baseApiUrl = `/api/kibana/management/saved_objects/relationships`; + const coerceToArray = itemOrItems => [].concat(itemOrItems); + const getSavedObjectTypesQuery = types => coerceToArray(types).map(type => `savedObjectTypes=${type}`).join('&'); + const defaultQuery = getSavedObjectTypesQuery(['visualization', 'index-pattern', 'search', 'dashboard']); describe('searches', async () => { it('should validate search response schema', async () => { await supertest - .get(`/api/kibana/management/saved_objects/relationships/search/960372e0-3224-11e8-a572-ffca06da1357`) + .get(`${baseApiUrl}/search/960372e0-3224-11e8-a572-ffca06da1357?${defaultQuery}`) .expect(200) .then(resp => { const validationResult = Joi.validate(resp.body, SEARCH_RESPONSE_SCHEMA); @@ -57,7 +61,7 @@ export default function ({ getService }) { it('should work for searches', async () => { await supertest - .get(`/api/kibana/management/saved_objects/relationships/search/960372e0-3224-11e8-a572-ffca06da1357`) + .get(`${baseApiUrl}/search/960372e0-3224-11e8-a572-ffca06da1357?${defaultQuery}`) .expect(200) .then(resp => { expect(resp.body).to.eql({ @@ -77,9 +81,26 @@ export default function ({ getService }) { }); }); + it('should filter based on savedObjectTypes', async () => { + await supertest + .get(`${baseApiUrl}/search/960372e0-3224-11e8-a572-ffca06da1357?${getSavedObjectTypesQuery('visualization')}`) + .expect(res => console.log(res.text)) + .expect(200) + .then(resp => { + expect(resp.body).to.eql({ + visualization: [ + { + id: 'a42c0580-3224-11e8-a572-ffca06da1357', + title: 'VisualizationFromSavedSearch', + }, + ] + }); + }); + }); + //TODO: https://github.com/elastic/kibana/issues/19713 causes this test to fail. it.skip('should return 404 if search finds no results', async () => { - await supertest.get(`/api/kibana/management/saved_objects/relationships/search/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`).expect(404); + await supertest.get(`${baseApiUrl}/search/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx${defaultQuery}`).expect(404); }); }); @@ -90,7 +111,7 @@ export default function ({ getService }) { it('should validate dashboard response schema', async () => { await supertest - .get(`/api/kibana/management/saved_objects/relationships/dashboard/b70c7ae0-3224-11e8-a572-ffca06da1357`) + .get(`${baseApiUrl}/dashboard/b70c7ae0-3224-11e8-a572-ffca06da1357?${defaultQuery}`) .expect(200) .then(resp => { const validationResult = Joi.validate(resp.body, DASHBOARD_RESPONSE_SCHEMA); @@ -100,7 +121,7 @@ export default function ({ getService }) { it('should work for dashboards', async () => { await supertest - .get(`/api/kibana/management/saved_objects/relationships/dashboard/b70c7ae0-3224-11e8-a572-ffca06da1357`) + .get(`${baseApiUrl}/dashboard/b70c7ae0-3224-11e8-a572-ffca06da1357?${defaultQuery}`) .expect(200) .then(resp => { expect(resp.body).to.eql({ @@ -118,10 +139,19 @@ export default function ({ getService }) { }); }); + it('should filter based on savedObjectTypes', async () => { + await supertest + .get(`${baseApiUrl}/dashboard/b70c7ae0-3224-11e8-a572-ffca06da1357?${getSavedObjectTypesQuery('search')}`) + .expect(200) + .then(resp => { + expect(resp.body).to.eql({}); + }); + }); + //TODO: https://github.com/elastic/kibana/issues/19713 causes this test to fail. it.skip('should return 404 if dashboard finds no results', async () => { await supertest - .get(`/api/kibana/management/saved_objects/relationships/dashboard/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`) + .get(`${baseApiUrl}/dashboard/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx${defaultQuery}`) .expect(404); }); }); @@ -134,7 +164,7 @@ export default function ({ getService }) { it('should validate visualization response schema', async () => { await supertest - .get(`/api/kibana/management/saved_objects/relationships/visualization/a42c0580-3224-11e8-a572-ffca06da1357`) + .get(`${baseApiUrl}/visualization/a42c0580-3224-11e8-a572-ffca06da1357?${defaultQuery}`) .expect(200) .then(resp => { const validationResult = Joi.validate(resp.body, VISUALIZATIONS_RESPONSE_SCHEMA); @@ -144,7 +174,7 @@ export default function ({ getService }) { it('should work for visualizations', async () => { await supertest - .get(`/api/kibana/management/saved_objects/relationships/visualization/a42c0580-3224-11e8-a572-ffca06da1357`) + .get(`${baseApiUrl}/visualization/a42c0580-3224-11e8-a572-ffca06da1357?${defaultQuery}`) .expect(200) .then(resp => { expect(resp.body).to.eql({ @@ -164,9 +194,25 @@ export default function ({ getService }) { }); }); + it('should filter based on savedObjectTypes', async () => { + await supertest + .get(`${baseApiUrl}/visualization/a42c0580-3224-11e8-a572-ffca06da1357?${getSavedObjectTypesQuery('search')}`) + .expect(200) + .then(resp => { + expect(resp.body).to.eql({ + search: [ + { + id: '960372e0-3224-11e8-a572-ffca06da1357', + title: 'OneRecord' + }, + ] + }); + }); + }); + it('should return 404 if visualizations finds no results', async () => { await supertest - .get(`/api/kibana/management/saved_objects/relationships/visualization/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`) + .get(`${baseApiUrl}/visualization/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?${defaultQuery}`) .expect(404); }); }); @@ -179,7 +225,7 @@ export default function ({ getService }) { it('should validate visualization response schema', async () => { await supertest - .get(`/api/kibana/management/saved_objects/relationships/index-pattern/8963ca30-3224-11e8-a572-ffca06da1357`) + .get(`${baseApiUrl}/index-pattern/8963ca30-3224-11e8-a572-ffca06da1357?${defaultQuery}`) .expect(200) .then(resp => { const validationResult = Joi.validate(resp.body, INDEX_PATTERN_RESPONSE_SCHEMA); @@ -189,7 +235,7 @@ export default function ({ getService }) { it('should work for index patterns', async () => { await supertest - .get(`/api/kibana/management/saved_objects/relationships/index-pattern/8963ca30-3224-11e8-a572-ffca06da1357`) + .get(`${baseApiUrl}/index-pattern/8963ca30-3224-11e8-a572-ffca06da1357?${defaultQuery}`) .expect(200) .then(resp => { expect(resp.body).to.eql({ @@ -209,9 +255,25 @@ export default function ({ getService }) { }); }); + it('should filter based on savedObjectTypes', async () => { + await supertest + .get(`${baseApiUrl}/index-pattern/8963ca30-3224-11e8-a572-ffca06da1357?${getSavedObjectTypesQuery('search')}`) + .expect(200) + .then(resp => { + expect(resp.body).to.eql({ + search: [ + { + id: '960372e0-3224-11e8-a572-ffca06da1357', + title: 'OneRecord', + }, + ] + }); + }); + }); + it('should return 404 if index pattern finds no results', async () => { await supertest - .get(`/api/kibana/management/saved_objects/relationships/index-pattern/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`) + .get(`${baseApiUrl}/index-pattern/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?${defaultQuery}`) .expect(404); }); }); diff --git a/test/functional/config.js b/test/functional/config.js index 4b41ebe71c2c..ace10d40681a 100644 --- a/test/functional/config.js +++ b/test/functional/config.js @@ -64,6 +64,9 @@ export default async function ({ readConfigFile }) { }, apps: { + kibana: { + pathname: '/app/kibana', + }, status_page: { pathname: '/status', }, diff --git a/test/functional/page_objects/common_page.js b/test/functional/page_objects/common_page.js index 90a8689cfb51..b16d0d04531a 100644 --- a/test/functional/page_objects/common_page.js +++ b/test/functional/page_objects/common_page.js @@ -49,10 +49,14 @@ export function CommonPageProvider({ getService, getPageObjects }) { * @param {string} appName As defined in the apps config * @param {string} subUrl The route after the hash (#) */ - async navigateToUrl(appName, subUrl) { + async navigateToUrl(appName, subUrl, { + basePath = '', + ensureCurrentUrl = true, + shouldLoginIfPrompted = true + } = {}) { + // we onlt use the pathname from the appConfig and use the subUrl as the hash const appConfig = { - ...config.get(['apps', appName]), - // Overwrite the default hash with the URL we really want. + pathname: `${basePath}${config.get(['apps', appName]).pathname}`, hash: `${appName}/${subUrl}`, }; @@ -60,8 +64,38 @@ export function CommonPageProvider({ getService, getPageObjects }) { await retry.try(async () => { log.debug(`navigateToUrl ${appUrl}`); await browser.get(appUrl); - const currentUrl = await this.loginIfPrompted(appUrl); - if (!currentUrl.includes(appUrl)) { + + const currentUrl = shouldLoginIfPrompted ? await this.loginIfPrompted(appUrl) : await browser.getCurrentUrl(); + + if (ensureCurrentUrl && !currentUrl.includes(appUrl)) { + throw new Error(`expected ${currentUrl}.includes(${appUrl})`); + } + }); + } + + /** + * @param {string} appName As defined in the apps config + * @param {string} hash The route after the hash (#) + */ + async navigateToActualUrl(appName, hash, { + basePath = '', + ensureCurrentUrl = true, + shouldLoginIfPrompted = true + } = {}) { + // we only use the apps config to get the application path + const appConfig = { + pathname: `${basePath}${config.get(['apps', appName]).pathname}`, + hash, + }; + + const appUrl = getUrl.noAuth(config.get('servers.kibana'), appConfig); + await retry.try(async () => { + log.debug(`navigateToActualUrl ${appUrl}`); + await browser.get(appUrl); + + const currentUrl = shouldLoginIfPrompted ? await this.loginIfPrompted(appUrl) : await browser.getCurrentUrl(); + + if (ensureCurrentUrl && !currentUrl.includes(appUrl)) { throw new Error(`expected ${currentUrl}.includes(${appUrl})`); } }); @@ -89,17 +123,12 @@ export function CommonPageProvider({ getService, getPageObjects }) { return currentUrl; } - - /** - * @param {string} appName - name of the app - * @param {object} [opts] - optional options object - * @param {object} [opts.appConfig] - overrides for appConfig, e.g. { pathname, hash } - */ - navigateToApp(appName, opts = { appConfig: {} }) { + navigateToApp(appName, { basePath = '', shouldLoginIfPrompted = true, hash = '' } = {}) { const self = this; + const appConfig = config.get(['apps', appName]); const appUrl = getUrl.noAuth(config.get('servers.kibana'), { - ...config.get(['apps', appName]), - ...opts.appConfig, + pathname: `${basePath}${appConfig.pathname}`, + hash: hash || appConfig.hash, }); log.debug('navigating to ' + appName + ' url: ' + appUrl); @@ -133,13 +162,14 @@ export function CommonPageProvider({ getService, getPageObjects }) { return browser.refresh(); }) .then(async function () { - const currentUrl = await self.loginIfPrompted(appUrl); + const currentUrl = shouldLoginIfPrompted ? await self.loginIfPrompted(appUrl) : browser.getCurrentUrl(); if (currentUrl.includes('app/kibana')) { await testSubjects.find('kibanaChrome'); } }) .then(async function () { + const currentUrl = (await browser.getCurrentUrl()).replace(/\/\/\w+:\w+@/, '//'); const maxAdditionalLengthOnNavUrl = 230; // On several test failures at the end of the TileMap test we try to navigate back to @@ -327,6 +357,11 @@ export function CommonPageProvider({ getService, getPageObjects }) { } } } + + async getBodyText() { + const el = await find.byCssSelector('body>pre'); + return await el.getVisibleText(); + } } return new CommonPage(); diff --git a/test/functional/page_objects/dashboard_page.js b/test/functional/page_objects/dashboard_page.js index ee7d07e5c6f3..c8ec060c246f 100644 --- a/test/functional/page_objects/dashboard_page.js +++ b/test/functional/page_objects/dashboard_page.js @@ -18,7 +18,6 @@ */ import _ from 'lodash'; - import { DashboardConstants } from '../../../src/legacy/core_plugins/kibana/public/dashboard/dashboard_constants'; export const PIE_CHART_VIS_NAME = 'Visualization PieChart'; @@ -219,7 +218,7 @@ export function DashboardPageProvider({ getService, getPageObjects }) { } async clickNewDashboard() { - // newDashboardLink button is only visible when dashboard listing table is displayed + // newItemButton button is only visible when dashboard listing table is displayed // (at least one dashboard). const exists = await testSubjects.exists('newItemButton'); if (exists) { @@ -606,6 +605,10 @@ export function DashboardPageProvider({ getService, getPageObjects }) { return await testSubjects.click('dashboardPanelTitlesCheckbox'); } + async expectMissingSaveOption() { + await testSubjects.missingOrFail('dashboardSaveMenuItem'); + } + async getNotLoadedVisualizations(vizList) { const checkList = []; for (const name of vizList) { diff --git a/test/functional/page_objects/discover_page.js b/test/functional/page_objects/discover_page.js index 502dbf5de17c..2738482fcb52 100644 --- a/test/functional/page_objects/discover_page.js +++ b/test/functional/page_objects/discover_page.js @@ -236,6 +236,14 @@ export function DiscoverPageProvider({ getService, getPageObjects }) { }); } + async expectFieldListItemVisualize(field) { + await testSubjects.existOrFail(`fieldVisualize-${field}`); + } + + async expectMissingFieldListItemVisualize(field) { + await testSubjects.missingOrFail(`fieldVisualize-${field}`); + } + async clickFieldListPlusFilter(field, value) { // this method requires the field details to be open from clickFieldListItem() // testSubjects.find doesn't handle spaces in the data-test-subj value diff --git a/test/functional/page_objects/error_page.js b/test/functional/page_objects/error_page.js new file mode 100644 index 000000000000..dd4966ca690f --- /dev/null +++ b/test/functional/page_objects/error_page.js @@ -0,0 +1,48 @@ +/* + * 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 expect from '@kbn/expect'; + +export function ErrorPageProvider({ getPageObjects }) { + const PageObjects = getPageObjects(['common']); + + class ErrorPage { + async expectForbidden() { + const messageText = await PageObjects.common.getBodyText(); + expect(messageText).to.eql( + JSON.stringify({ + statusCode: 403, + error: 'Forbidden', + message: 'Forbidden' + }) + ); + } + async expectNotFound() { + const messageText = await PageObjects.common.getBodyText(); + expect(messageText).to.eql( + JSON.stringify({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }) + ); + } + } + + return new ErrorPage(); +} diff --git a/test/functional/page_objects/index.ts b/test/functional/page_objects/index.ts index 1c1e06d08c9d..1e8c454f42cf 100644 --- a/test/functional/page_objects/index.ts +++ b/test/functional/page_objects/index.ts @@ -28,6 +28,8 @@ import { DashboardPageProvider } from './dashboard_page'; // @ts-ignore not TS yet import { DiscoverPageProvider } from './discover_page'; // @ts-ignore not TS yet +import { ErrorPageProvider } from './error_page'; +// @ts-ignore not TS yet import { HeaderPageProvider } from './header_page'; // @ts-ignore not TS yet import { HomePageProvider } from './home_page'; @@ -55,6 +57,7 @@ export const pageObjects = { context: ContextPageProvider, dashboard: DashboardPageProvider, discover: DiscoverPageProvider, + error: ErrorPageProvider, header: HeaderPageProvider, home: HomePageProvider, monitoring: MonitoringPageProvider, diff --git a/test/functional/page_objects/settings_page.js b/test/functional/page_objects/settings_page.js index 2b42906717f3..a25257e3ba1d 100644 --- a/test/functional/page_objects/settings_page.js +++ b/test/functional/page_objects/settings_page.js @@ -66,6 +66,11 @@ export function SettingsPageProvider({ getService, getPageObjects }) { return await setting.getProperty('value'); } + async expectDisabledAdvancedSetting(propertyName) { + const setting = await testSubjects.find(`advancedSetting-editField-${propertyName}`); + expect(setting.getAttribute('disabled')).to.eql(''); + } + async getAdvancedSettingCheckbox(propertyName) { log.debug('in getAdvancedSettingCheckbox'); return await testSubjects.getProperty(`advancedSetting-editField-${propertyName}`, 'checked'); @@ -607,6 +612,50 @@ export function SettingsPageProvider({ getService, getPageObjects }) { return objects; } + + async getSavedObjectsTableSummary() { + const table = await testSubjects.find('savedObjectsTable'); + const rows = await table.findAllByCssSelector('tbody tr'); + + const summary = []; + for (const row of rows) { + const titleCell = await row.findByCssSelector('td:nth-child(3)'); + const title = await titleCell.getVisibleText(); + + + const viewInAppButtons = await row.findAllByCssSelector('[aria-label="In app"]'); + const canViewInApp = Boolean(viewInAppButtons.length); + summary.push({ + title, + canViewInApp, + }); + } + + return summary; + } + + async clickSavedObjectsTableSelectAll() { + const checkboxSelectAll = await testSubjects.find('checkboxSelectAll'); + await checkboxSelectAll.click(); + } + + async canSavedObjectsBeDeleted() { + const deleteButton = await testSubjects.find('savedObjectsManagementDelete'); + return await deleteButton.isEnabled(); + } + + async canSavedObjectBeDeleted(id) { + const allCheckBoxes = await testSubjects.findAll('checkboxSelectRow*'); + for (const checkBox of allCheckBoxes) { + if (await checkBox.isSelected()) { + await checkBox.click(); + } + } + + const checkBox = await testSubjects.find(`checkboxSelectRow-${id}`); + await checkBox.click(); + return await this.canSavedObjectsBeDeleted(); + } } return new SettingsPage(); diff --git a/test/functional/page_objects/share_page.js b/test/functional/page_objects/share_page.js index b9be3faae640..0bd2def01027 100644 --- a/test/functional/page_objects/share_page.js +++ b/test/functional/page_objects/share_page.js @@ -53,6 +53,14 @@ export function SharePageProvider({ getService, getPageObjects }) { return await testSubjects.getAttribute('copyShareUrlButton', 'data-share-url'); } + async createShortUrlExistOrFail() { + await testSubjects.existOrFail('createShortUrl'); + } + + async createShortUrlMissingOrFail() { + await testSubjects.missingOrFail('createShortUrl'); + } + async checkShortenUrl() { const shareForm = await testSubjects.find('shareUrlForm'); await PageObjects.visualize.checkCheckbox('useShortUrl'); diff --git a/test/functional/page_objects/timelion_page.js b/test/functional/page_objects/timelion_page.js index 7ca7af8a6ef1..cae1343d0508 100644 --- a/test/functional/page_objects/timelion_page.js +++ b/test/functional/page_objects/timelion_page.js @@ -68,6 +68,22 @@ export function TimelionPageProvider({ getService, getPageObjects }) { // Wait for timelion expression to be updated after clicking suggestions await PageObjects.common.sleep(waitTime); } + + async saveTimelionSheet() { + await testSubjects.click('timelionSaveButton'); + await testSubjects.click('timelionSaveAsSheetButton'); + await testSubjects.click('timelionFinishSaveButton'); + } + + async expectWriteControls() { + await testSubjects.existOrFail('timelionSaveButton'); + await testSubjects.existOrFail('timelionDeleteButton'); + } + + async expectMissingWriteControls() { + await testSubjects.missingOrFail('timelionSaveButton'); + await testSubjects.missingOrFail('timelionDeleteButton'); + } } return new TimelionPage(); diff --git a/test/functional/page_objects/visualize_page.js b/test/functional/page_objects/visualize_page.js index 2b0e922d698e..9e062beaadb6 100644 --- a/test/functional/page_objects/visualize_page.js +++ b/test/functional/page_objects/visualize_page.js @@ -764,12 +764,9 @@ export function VisualizePageProvider({ getService, getPageObjects, updateBaseli )); } - async saveVisualizationExpectFail(vizName, { saveAsNew = false } = {}) { - await this.saveVisualization(vizName, { saveAsNew }); - const errorToast = await testSubjects.exists('saveVisualizationError', { - timeout: defaultFindTimeout - }); - expect(errorToast).to.be(true); + async expectNoSaveOption() { + const saveButtonExists = await testSubjects.exists('visualizeSaveButton'); + expect(saveButtonExists).to.be(false); } async clickLoadSavedVisButton() { diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js index ad5b2fde1acf..5386fdc1251c 100644 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ b/x-pack/dev-tools/jest/create_jest_config.js @@ -13,6 +13,7 @@ export function createJestConfig({ roots: [ '/plugins', '/server', + '/public', ], moduleFileExtensions: [ 'js', diff --git a/x-pack/package.json b/x-pack/package.json index 773d732d7029..c909d7086be2 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -87,6 +87,7 @@ "chalk": "^2.4.1", "chance": "1.0.10", "checksum": "0.1.1", + "cheerio": "0.22.0", "commander": "2.12.2", "copy-webpack-plugin": "^4.5.2", "del": "^3.0.0", diff --git a/x-pack/plugins/__mocks__/ui/capabilities.ts b/x-pack/plugins/__mocks__/ui/capabilities.ts new file mode 100644 index 000000000000..5de2c6144fc6 --- /dev/null +++ b/x-pack/plugins/__mocks__/ui/capabilities.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UICapabilities } from 'ui/capabilities'; + +let internals: UICapabilities = { + navLinks: {}, + management: {}, + catalogue: {}, + spaces: { + manage: true, + }, +}; + +export const uiCapabilities = new Proxy( + {}, + { + get: (target, property) => { + return internals[String(property)] as any; + }, + } +); + +export function setMockCapabilities(mockCapabilities: UICapabilities) { + internals = mockCapabilities; +} diff --git a/x-pack/plugins/__mocks__/ui/chrome.js b/x-pack/plugins/__mocks__/ui/chrome.js index b85d345a68ce..bdba07fa08df 100644 --- a/x-pack/plugins/__mocks__/ui/chrome.js +++ b/x-pack/plugins/__mocks__/ui/chrome.js @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { uiCapabilities } from './capabilities'; + function getUiSettingsClient() { return { get: key => { @@ -33,6 +35,8 @@ function getInjected(key) { return 'apm*'; case 'mlEnabled': return true; + case 'uiCapabilities': + return uiCapabilities; case 'isCloudEnabled': return false; default: diff --git a/x-pack/plugins/apm/index.ts b/x-pack/plugins/apm/index.ts index 533071dde482..a97c63a51bf1 100644 --- a/x-pack/plugins/apm/index.ts +++ b/x-pack/plugins/apm/index.ts @@ -70,6 +70,37 @@ export function apm(kibana: any) { // TODO: get proper types init(server: Server) { + server.plugins.xpack_main.registerFeature({ + id: 'apm', + name: i18n.translate('xpack.apm.featureRegistry.apmFeatureName', { + defaultMessage: 'APM' + }), + icon: 'apmApp', + navLinkId: 'apm', + app: ['apm', 'kibana'], + catalogue: ['apm'], + privileges: { + all: { + api: ['apm'], + catalogue: ['apm'], + savedObject: { + all: [], + read: ['config'] + }, + ui: ['show'] + }, + read: { + api: ['apm'], + catalogue: ['apm'], + savedObject: { + all: [], + read: ['config'] + }, + ui: ['show'] + } + } + }); + const initializerContext = {} as PluginInitializerContext; const core = { http: { diff --git a/x-pack/plugins/apm/public/components/app/Main/index.tsx b/x-pack/plugins/apm/public/components/app/Main/index.tsx index 908a0f021164..a34d8da60974 100644 --- a/x-pack/plugins/apm/public/components/app/Main/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Main/index.tsx @@ -25,7 +25,7 @@ const MainContainer = styled.div` export function Main() { return ( - + diff --git a/x-pack/plugins/apm/server/routes/errors.ts b/x-pack/plugins/apm/server/routes/errors.ts index e4f855f298e9..3bd7be240eb4 100644 --- a/x-pack/plugins/apm/server/routes/errors.ts +++ b/x-pack/plugins/apm/server/routes/errors.ts @@ -32,7 +32,8 @@ export function initErrorsApi(core: CoreSetup) { sortField: Joi.string(), sortDirection: Joi.string() }) - } + }, + tags: ['access:apm'] }, handler: req => { const setup = setupRequest(req); @@ -57,7 +58,8 @@ export function initErrorsApi(core: CoreSetup) { options: { validate: { query: withDefaultValidators() - } + }, + tags: ['access:apm'] }, handler: req => { const setup = setupRequest(req); @@ -83,7 +85,8 @@ export function initErrorsApi(core: CoreSetup) { options: { validate: { query: withDefaultValidators() - } + }, + tags: ['access:apm'] }, handler: distributionHandler }); @@ -94,7 +97,8 @@ export function initErrorsApi(core: CoreSetup) { options: { validate: { query: withDefaultValidators() - } + }, + tags: ['access:apm'] }, handler: distributionHandler }); diff --git a/x-pack/plugins/apm/server/routes/metrics.ts b/x-pack/plugins/apm/server/routes/metrics.ts index c94defca8a5f..997ff9b803c0 100644 --- a/x-pack/plugins/apm/server/routes/metrics.ts +++ b/x-pack/plugins/apm/server/routes/metrics.ts @@ -24,7 +24,8 @@ export function initMetricsApi(core: CoreSetup) { options: { validate: { query: withDefaultValidators() - } + }, + tags: ['access:apm'] }, handler: async req => { const setup = setupRequest(req); diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index 345dda5da68c..0bbc1b8be894 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -28,7 +28,8 @@ export function initServicesApi(core: CoreSetup) { options: { validate: { query: withDefaultValidators() - } + }, + tags: ['access:apm'] }, handler: async req => { const setup = setupRequest(req); @@ -51,7 +52,8 @@ export function initServicesApi(core: CoreSetup) { options: { validate: { query: withDefaultValidators() - } + }, + tags: ['access:apm'] }, handler: req => { const setup = setupRequest(req); diff --git a/x-pack/plugins/apm/server/routes/traces.ts b/x-pack/plugins/apm/server/routes/traces.ts index 112c79110d37..5bc996b1b71c 100644 --- a/x-pack/plugins/apm/server/routes/traces.ts +++ b/x-pack/plugins/apm/server/routes/traces.ts @@ -29,7 +29,8 @@ export function initTracesApi(core: CoreSetup) { options: { validate: { query: withDefaultValidators() - } + }, + tags: ['access:apm'] }, handler: req => { const setup = setupRequest(req); @@ -45,7 +46,8 @@ export function initTracesApi(core: CoreSetup) { options: { validate: { query: withDefaultValidators() - } + }, + tags: ['access:apm'] }, handler: req => { const { traceId } = req.params; diff --git a/x-pack/plugins/apm/server/routes/transaction_groups.ts b/x-pack/plugins/apm/server/routes/transaction_groups.ts index 71edd0ba3867..5c93edf5bd86 100644 --- a/x-pack/plugins/apm/server/routes/transaction_groups.ts +++ b/x-pack/plugins/apm/server/routes/transaction_groups.ts @@ -31,7 +31,8 @@ export function initTransactionGroupsApi(core: CoreSetup) { query: withDefaultValidators({ query: Joi.string() }) - } + }, + tags: ['access:apm'] }, handler: req => { const { serviceName, transactionType } = req.params; @@ -51,7 +52,8 @@ export function initTransactionGroupsApi(core: CoreSetup) { options: { validate: { query: withDefaultValidators() - } + }, + tags: ['access:apm'] }, handler: req => { const setup = setupRequest(req); @@ -71,7 +73,8 @@ export function initTransactionGroupsApi(core: CoreSetup) { options: { validate: { query: withDefaultValidators() - } + }, + tags: ['access:apm'] }, handler: req => { const setup = setupRequest(req); @@ -90,7 +93,8 @@ export function initTransactionGroupsApi(core: CoreSetup) { options: { validate: { query: withDefaultValidators() - } + }, + tags: ['access:apm'] }, handler: req => { const setup = setupRequest(req); @@ -114,7 +118,8 @@ export function initTransactionGroupsApi(core: CoreSetup) { transactionId: Joi.string().default(''), traceId: Joi.string().default('') }) - } + }, + tags: ['access:apm'] }, handler: req => { const setup = setupRequest(req); diff --git a/x-pack/plugins/canvas/init.js b/x-pack/plugins/canvas/init.js index ef06aa9eaafa..9feff1fe12ac 100644 --- a/x-pack/plugins/canvas/init.js +++ b/x-pack/plugins/canvas/init.js @@ -35,6 +35,31 @@ export default async function(server /*options*/) { }; }); + server.plugins.xpack_main.registerFeature({ + id: 'canvas', + name: 'Canvas', + icon: 'canvasApp', + navLinkId: 'canvas', + app: ['canvas', 'kibana'], + catalogue: ['canvas'], + privileges: { + all: { + savedObject: { + all: ['canvas-workpad'], + read: ['config', 'index-pattern'], + }, + ui: ['save'], + }, + read: { + savedObject: { + all: [], + read: ['config', 'index-pattern', 'canvas-workpad'], + }, + ui: [], + }, + }, + }); + registerCanvasUsageCollector(server); loadSampleData(server); routes(server); diff --git a/x-pack/plugins/canvas/public/apps/workpad/routes.js b/x-pack/plugins/canvas/public/apps/workpad/routes.js index 996df0be1f90..0618a3907592 100644 --- a/x-pack/plugins/canvas/public/apps/workpad/routes.js +++ b/x-pack/plugins/canvas/public/apps/workpad/routes.js @@ -12,8 +12,6 @@ import { setWorkpad } from '../../state/actions/workpad'; import { setAssets, resetAssets } from '../../state/actions/assets'; import { setPage } from '../../state/actions/pages'; import { getWorkpad } from '../../state/selectors/workpad'; -import { isFirstLoad } from '../../state/selectors/app'; -import { setCanUserWrite, setFirstLoad } from '../../state/actions/transient'; import { WorkpadApp } from './workpad_app'; export const routes = [ @@ -32,11 +30,6 @@ export const routes = [ router.redirectTo('loadWorkpad', { id: newWorkpad.id, page: 1 }); } catch (err) { notify.error(err, { title: `Couldn't create workpad` }); - // TODO: remove this and switch to checking user privileges when canvas loads when granular app privileges are introduced - // https://github.com/elastic/kibana/issues/20277 - if (err.response && err.response.status === 403) { - dispatch(setCanUserWrite(false)); - } router.redirectTo('home'); } }, @@ -51,23 +44,10 @@ export const routes = [ // load workpad if given a new id via url param const state = getState(); const currentWorkpad = getWorkpad(state); - const firstLoad = isFirstLoad(state); if (params.id !== currentWorkpad.id) { try { const fetchedWorkpad = await workpadService.get(params.id); - // tests if user has permissions to write to workpads - // TODO: remove this and switch to checking user privileges when canvas loads when granular app privileges are introduced - // https://github.com/elastic/kibana/issues/20277 - if (firstLoad) { - await workpadService.update(params.id, fetchedWorkpad).catch(err => { - if (err.response && err.response.status === 403) { - dispatch(setCanUserWrite(false)); - } - }); - dispatch(setFirstLoad(false)); - } - const { assets, ...workpad } = fetchedWorkpad; dispatch(setWorkpad(workpad)); dispatch(setAssets(assets)); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.js b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.js index 173b346143cf..0d494164fa6c 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.js +++ b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.js @@ -141,6 +141,7 @@ export class WorkpadHeader extends React.PureComponent { fill size="s" iconType="vector" + data-test-subj="add-element-button" onClick={() => setShowElementModal(true)} > Add element diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/index.js b/x-pack/plugins/canvas/public/components/workpad_loader/index.js index 31ea8f7d41e5..fb1150dd3390 100644 --- a/x-pack/plugins/canvas/public/components/workpad_loader/index.js +++ b/x-pack/plugins/canvas/public/components/workpad_loader/index.js @@ -12,7 +12,6 @@ import { notify } from '../../lib/notify'; import { canUserWrite } from '../../state/selectors/app'; import { getWorkpad } from '../../state/selectors/workpad'; import { getId } from '../../lib/get_id'; -import { setCanUserWrite } from '../../state/actions/transient'; import { downloadWorkpad } from '../../lib/download_workpad'; import { WorkpadLoader as Component } from './workpad_loader'; @@ -21,18 +20,11 @@ const mapStateToProps = state => ({ canUserWrite: canUserWrite(state), }); -const mapDispatchToProps = dispatch => ({ - setCanUserWrite: canUserWrite => dispatch(setCanUserWrite(canUserWrite)), -}); - export const WorkpadLoader = compose( getContext({ router: PropTypes.object, }), - connect( - mapStateToProps, - mapDispatchToProps - ), + connect(mapStateToProps), withState('workpads', 'setWorkpads', null), withHandlers({ // Workpad creation via navigation @@ -44,11 +36,6 @@ export const WorkpadLoader = compose( props.router.navigateTo('loadWorkpad', { id: workpad.id, page: 1 }); } catch (err) { notify.error(err, { title: `Couldn't upload workpad` }); - // TODO: remove this and switch to checking user privileges when canvas loads when granular app privileges are introduced - // https://github.com/elastic/kibana/issues/20277 - if (err.response && err.response.status === 403) { - props.setCanUserWrite(false); - } } return; } @@ -79,11 +66,6 @@ export const WorkpadLoader = compose( props.router.navigateTo('loadWorkpad', { id: workpad.id, page: 1 }); } catch (err) { notify.error(err, { title: `Couldn't clone workpad` }); - // TODO: remove this and switch to checking user privileges when canvas loads when granular app privileges are introduced - // https://github.com/elastic/kibana/issues/20277 - if (err.response && err.response.status === 403) { - props.setCanUserWrite(false); - } } }, @@ -112,11 +94,6 @@ export const WorkpadLoader = compose( if (result.err) { errors.push(result.id); - // TODO: remove this and switch to checking user privileges when canvas loads when granular app privileges are introduced - // https://github.com/elastic/kibana/issues/20277 - if (result.err.response && result.err.response.status === 403) { - props.setCanUserWrite(false); - } } else { passes.push(result.id); } diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_create.js b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_create.js index 8feedeb7546d..ad46f25669e8 100644 --- a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_create.js +++ b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_create.js @@ -9,7 +9,14 @@ import PropTypes from 'prop-types'; import { EuiButton } from '@elastic/eui'; export const WorkpadCreate = ({ createPending, onCreate, ...rest }) => ( - + Create workpad ); diff --git a/x-pack/plugins/canvas/public/state/actions/transient.js b/x-pack/plugins/canvas/public/state/actions/transient.js index 20e467ff06fe..a87c39b7ef6e 100644 --- a/x-pack/plugins/canvas/public/state/actions/transient.js +++ b/x-pack/plugins/canvas/public/state/actions/transient.js @@ -6,7 +6,6 @@ import { createAction } from 'redux-actions'; -export const setCanUserWrite = createAction('setCanUserWrite'); export const setFullscreen = createAction('setFullscreen'); export const selectToplevelNodes = createAction('selectToplevelNodes'); export const setFirstLoad = createAction('setFirstLoad'); diff --git a/x-pack/plugins/canvas/public/state/initial_state.js b/x-pack/plugins/canvas/public/state/initial_state.js index ade6f4698508..bb814f8c30d9 100644 --- a/x-pack/plugins/canvas/public/state/initial_state.js +++ b/x-pack/plugins/canvas/public/state/initial_state.js @@ -5,6 +5,7 @@ */ import { get } from 'lodash'; +import { uiCapabilities } from 'ui/capabilities'; import { getDefaultWorkpad } from './defaults'; export const getInitialState = path => { @@ -12,8 +13,7 @@ export const getInitialState = path => { app: {}, // Kibana stuff in here assets: {}, // assets end up here transient: { - isFirstLoad: true, - canUserWrite: true, + canUserWrite: uiCapabilities.canvas.save, elementStats: { total: 0, ready: 0, diff --git a/x-pack/plugins/canvas/public/state/reducers/transient.js b/x-pack/plugins/canvas/public/state/reducers/transient.js index bb40404da488..43b2c9ccc188 100644 --- a/x-pack/plugins/canvas/public/state/reducers/transient.js +++ b/x-pack/plugins/canvas/public/state/reducers/transient.js @@ -29,10 +29,6 @@ export const transientReducer = handleActions( ); }, - [transientActions.setCanUserWrite]: (transientState, { payload }) => { - return set(transientState, 'canUserWrite', Boolean(payload)); - }, - [transientActions.setFirstLoad]: (transientState, { payload }) => { return set(transientState, 'isFirstLoad', Boolean(payload)); }, diff --git a/x-pack/plugins/canvas/public/state/selectors/app.js b/x-pack/plugins/canvas/public/state/selectors/app.js index 26e5fff11b78..0f13df85a035 100644 --- a/x-pack/plugins/canvas/public/state/selectors/app.js +++ b/x-pack/plugins/canvas/public/state/selectors/app.js @@ -11,10 +11,6 @@ export function canUserWrite(state) { return get(state, 'transient.canUserWrite', true); } -export function isFirstLoad(state) { - return get(state, 'transient.isFirstLoad', true); -} - export function getFullscreen(state) { return get(state, 'transient.fullscreen', false); } diff --git a/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_add.test.js b/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_add.test.js index 26175c37f69f..8024696f53d9 100644 --- a/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_add.test.js +++ b/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_add.test.js @@ -14,6 +14,15 @@ import routing from '../../public/app/services/routing'; jest.mock('ui/chrome', () => ({ addBasePath: (path) => path || 'api/cross_cluster_replication', breadcrumbs: { set: () => {} }, + getInjected: (key) => { + if (key === 'uiCapabilities') { + return { + navLinks: {}, + management: {}, + catalogue: {} + }; + } + } })); jest.mock('ui/index_patterns', () => { diff --git a/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_edit.test.js b/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_edit.test.js index 698a20cbd8f0..9ec603087be8 100644 --- a/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_edit.test.js +++ b/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_edit.test.js @@ -15,6 +15,15 @@ import routing from '../../public/app/services/routing'; jest.mock('ui/chrome', () => ({ addBasePath: (path) => path || 'api/cross_cluster_replication', breadcrumbs: { set: () => {} }, + getInjected: (key) => { + if (key === 'uiCapabilities') { + return { + navLinks: {}, + management: {}, + catalogue: {} + }; + } + } })); jest.mock('ui/index_patterns', () => { diff --git a/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_add.test.js b/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_add.test.js index f7617bb79738..53aeaebdd314 100644 --- a/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_add.test.js +++ b/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_add.test.js @@ -17,6 +17,15 @@ import routing from '../../public/app/services/routing'; jest.mock('ui/chrome', () => ({ addBasePath: (path) => path || 'api/cross_cluster_replication', breadcrumbs: { set: () => {} }, + getInjected: (key) => { + if (key === 'uiCapabilities') { + return { + navLinks: {}, + management: {}, + catalogue: {} + }; + } + } })); jest.mock('ui/index_patterns', () => { diff --git a/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_edit.test.js b/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_edit.test.js index 80febd94af67..b45750a37f18 100644 --- a/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_edit.test.js +++ b/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_edit.test.js @@ -15,6 +15,15 @@ import routing from '../../public/app/services/routing'; jest.mock('ui/chrome', () => ({ addBasePath: (path) => path || 'api/cross_cluster_replication', breadcrumbs: { set: () => {} }, + getInjected: (key) => { + if (key === 'uiCapabilities') { + return { + navLinks: {}, + management: {}, + catalogue: {} + }; + } + } })); jest.mock('ui/index_patterns', () => { diff --git a/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/home.test.js b/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/home.test.js index 3c8372ffba8d..9e87b013394d 100644 --- a/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/home.test.js +++ b/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/home.test.js @@ -14,6 +14,16 @@ import routing from '../../public/app/services/routing'; jest.mock('ui/chrome', () => ({ addBasePath: () => 'api/cross_cluster_replication', breadcrumbs: { set: () => {} }, + getInjected: (key) => { + if (key === 'uiCapabilities') { + return { + navLinks: {}, + management: {}, + catalogue: {} + }; + } + throw new Error(`Unexpected call to chrome.getInjected with key ${key}`); + } })); jest.mock('ui/index_patterns', () => { diff --git a/x-pack/plugins/graph/index.js b/x-pack/plugins/graph/index.js index aa11b433475c..d2799157c99c 100644 --- a/x-pack/plugins/graph/index.js +++ b/x-pack/plugins/graph/index.js @@ -5,7 +5,7 @@ */ import { resolve } from 'path'; -import Boom from 'boom'; +import { i18n } from '@kbn/i18n'; import migrations from './migrations'; import { initServer } from './server'; @@ -49,6 +49,33 @@ export function graph(kibana) { }; }); + server.plugins.xpack_main.registerFeature({ + id: 'graph', + name: i18n.translate('xpack.graph.featureRegistry.graphFeatureName', { + defaultMessage: 'Graph', + }), + icon: 'graphApp', + navLinkId: 'graph', + app: ['graph', 'kibana'], + catalogue: ['graph'], + privileges: { + all: { + savedObject: { + all: ['graph-workspace'], + read: ['config', 'index-pattern'], + }, + ui: ['save', 'delete'], + }, + read: { + savedObject: { + all: [], + read: ['config', 'index-pattern', 'graph-workspace'], + }, + ui: [], + } + } + }); + initServer(server); }, }); diff --git a/x-pack/plugins/graph/public/app.js b/x-pack/plugins/graph/public/app.js index 305a229a80fc..b5bbfa5d5c5c 100644 --- a/x-pack/plugins/graph/public/app.js +++ b/x-pack/plugins/graph/public/app.js @@ -47,6 +47,7 @@ import { import { getOutlinkEncoders, } from './services/outlink_encoders'; +import { uiCapabilities } from 'ui/capabilities'; const app = uiModules.get('app/graph'); @@ -796,35 +797,43 @@ app.controller('graphuiPlugin', function ($scope, $route, $http, kbnUrl, Private }), run: function () {canWipeWorkspace(function () {kbnUrl.change('/home', {}); }); }, }); - if (!$scope.allSavingDisabled) { - $scope.topNavMenu.push({ - key: 'save', - label: i18n('xpack.graph.topNavMenu.saveWorkspace.enabledLabel', { - defaultMessage: 'Save', - }), - description: i18n('xpack.graph.topNavMenu.saveWorkspace.enabledAriaLabel', { - defaultMessage: 'Save Workspace', - }), - tooltip: i18n('xpack.graph.topNavMenu.saveWorkspace.enabledTooltip', { - defaultMessage: 'Save this workspace', - }), - disableButton: function () {return $scope.selectedFields.length === 0;}, - template: require('./templates/save_workspace.html') - }); - }else { - $scope.topNavMenu.push({ - key: 'save', - label: i18n('xpack.graph.topNavMenu.saveWorkspace.disabledLabel', { - defaultMessage: 'Save', - }), - description: i18n('xpack.graph.topNavMenu.saveWorkspace.disabledAriaLabel', { - defaultMessage: 'Save Workspace', - }), - tooltip: i18n('xpack.graph.topNavMenu.saveWorkspace.disabledTooltip', { - defaultMessage: 'No changes to saved workspaces are permitted by the current save policy', - }), - disableButton: true - }); + + // if saving is disabled using uiCapabilities, we don't want to render the save + // button so it's consistent with all of the other applications + if (uiCapabilities.graph.save) { + // allSavingDisabled is based on the xpack.graph.savePolicy, we'll maintain this functionality + if (!$scope.allSavingDisabled) { + $scope.topNavMenu.push({ + key: 'save', + label: i18n('xpack.graph.topNavMenu.saveWorkspace.enabledLabel', { + defaultMessage: 'Save', + }), + description: i18n('xpack.graph.topNavMenu.saveWorkspace.enabledAriaLabel', { + defaultMessage: 'Save Workspace', + }), + tooltip: i18n('xpack.graph.topNavMenu.saveWorkspace.enabledTooltip', { + defaultMessage: 'Save this workspace', + }), + disableButton: function () {return $scope.selectedFields.length === 0;}, + template: require('./templates/save_workspace.html'), + testId: 'graphSaveButton', + }); + } else { + $scope.topNavMenu.push({ + key: 'save', + label: i18n('xpack.graph.topNavMenu.saveWorkspace.disabledLabel', { + defaultMessage: 'Save', + }), + description: i18n('xpack.graph.topNavMenu.saveWorkspace.disabledAriaLabel', { + defaultMessage: 'Save Workspace', + }), + tooltip: i18n('xpack.graph.topNavMenu.saveWorkspace.disabledTooltip', { + defaultMessage: 'No changes to saved workspaces are permitted by the current save policy', + }), + disableButton: true, + testId: 'graphSaveButton', + }); + } } $scope.topNavMenu.push({ key: 'open', @@ -837,65 +846,74 @@ app.controller('graphuiPlugin', function ($scope, $route, $http, kbnUrl, Private tooltip: i18n('xpack.graph.topNavMenu.loadWorkspaceTooltip', { defaultMessage: 'Load a saved workspace', }), - template: require('./templates/load_workspace.html') + template: require('./templates/load_workspace.html'), + testId: 'graphOpenButton', }); - if (!$scope.allSavingDisabled) { - $scope.topNavMenu.push({ - key: 'delete', - disableButton: function () { - return $route.current.locals === undefined || $route.current.locals.savedWorkspace === undefined; - }, - label: i18n('xpack.graph.topNavMenu.deleteWorkspace.enabledLabel', { - defaultMessage: 'Delete', - }), - description: i18n('xpack.graph.topNavMenu.deleteWorkspace.enabledAriaLabel', { - defaultMessage: 'Delete Saved Workspace', - }), - tooltip: i18n('xpack.graph.topNavMenu.deleteWorkspace.enabledAriaTooltip', { - defaultMessage: 'Delete this workspace', - }), - run: function () { - const title = $route.current.locals.savedWorkspace.title; - function doDelete() { - $route.current.locals.SavedWorkspacesProvider.delete($route.current.locals.savedWorkspace.id); - kbnUrl.change('/home', {}); - - toastNotifications.addSuccess( - i18n('xpack.graph.topNavMenu.deleteWorkspaceNotification', { - defaultMessage: `Deleted '{workspaceTitle}'`, - values: { workspaceTitle: title }, - }) + // if deleting is disabled using uiCapabilities, we don't want to render the delete + // button so it's consistent with all of the other applications + if (uiCapabilities.graph.delete) { + + // allSavingDisabled is based on the xpack.graph.savePolicy, we'll maintain this functionality + if (!$scope.allSavingDisabled) { + $scope.topNavMenu.push({ + key: 'delete', + disableButton: function () { + return $route.current.locals === undefined || $route.current.locals.savedWorkspace === undefined; + }, + label: i18n('xpack.graph.topNavMenu.deleteWorkspace.enabledLabel', { + defaultMessage: 'Delete', + }), + description: i18n('xpack.graph.topNavMenu.deleteWorkspace.enabledAriaLabel', { + defaultMessage: 'Delete Saved Workspace', + }), + tooltip: i18n('xpack.graph.topNavMenu.deleteWorkspace.enabledAriaTooltip', { + defaultMessage: 'Delete this workspace', + }), + testId: 'graphDeleteButton', + run: function () { + const title = $route.current.locals.savedWorkspace.title; + function doDelete() { + $route.current.locals.SavedWorkspacesProvider.delete($route.current.locals.savedWorkspace.id); + kbnUrl.change('/home', {}); + + toastNotifications.addSuccess( + i18n('xpack.graph.topNavMenu.deleteWorkspaceNotification', { + defaultMessage: `Deleted '{workspaceTitle}'`, + values: { workspaceTitle: title }, + }) + ); + } + const confirmModalOptions = { + onConfirm: doDelete, + confirmButtonText: i18n('xpack.graph.topNavMenu.deleteWorkspace.confirmButtonLabel', { + defaultMessage: 'Delete workspace', + }), + }; + confirmModal( + i18n('xpack.graph.topNavMenu.deleteWorkspace.confirmText', { + defaultMessage: 'Are you sure you want to delete the workspace {title} ?', + values: { title }, + }), + confirmModalOptions ); } - const confirmModalOptions = { - onConfirm: doDelete, - confirmButtonText: i18n('xpack.graph.topNavMenu.deleteWorkspace.confirmButtonLabel', { - defaultMessage: 'Delete workspace', - }), - }; - confirmModal( - i18n('xpack.graph.topNavMenu.deleteWorkspace.confirmText', { - defaultMessage: 'Are you sure you want to delete the workspace {title} ?', - values: { title }, - }), - confirmModalOptions - ); - } - }); - }else { - $scope.topNavMenu.push({ - key: 'delete', - disableButton: true, - label: i18n('xpack.graph.topNavMenu.deleteWorkspace.disabledLabel', { - defaultMessage: 'Delete', - }), - description: i18n('xpack.graph.topNavMenu.deleteWorkspace.disabledAriaLabel', { - defaultMessage: 'Delete Saved Workspace', - }), - tooltip: i18n('xpack.graph.topNavMenu.deleteWorkspace.disabledTooltip', { - defaultMessage: 'No changes to saved workspaces are permitted by the current save policy', - }), - }); + }); + }else { + $scope.topNavMenu.push({ + key: 'delete', + disableButton: true, + label: i18n('xpack.graph.topNavMenu.deleteWorkspace.disabledLabel', { + defaultMessage: 'Delete', + }), + description: i18n('xpack.graph.topNavMenu.deleteWorkspace.disabledAriaLabel', { + defaultMessage: 'Delete Saved Workspace', + }), + tooltip: i18n('xpack.graph.topNavMenu.deleteWorkspace.disabledTooltip', { + defaultMessage: 'No changes to saved workspaces are permitted by the current save policy', + }), + testId: 'graphDeleteButton', + }); + } } $scope.topNavMenu.push({ key: 'settings', diff --git a/x-pack/plugins/grokdebugger/public/sections/grokdebugger/grokdebugger_route.js b/x-pack/plugins/grokdebugger/public/sections/grokdebugger/grokdebugger_route.js index 74530e73f379..aa05638403a9 100644 --- a/x-pack/plugins/grokdebugger/public/sections/grokdebugger/grokdebugger_route.js +++ b/x-pack/plugins/grokdebugger/public/sections/grokdebugger/grokdebugger_route.js @@ -5,14 +5,17 @@ */ import routes from 'ui/routes'; +import 'ui/capabilities/route_setup'; import { toastNotifications } from 'ui/notify'; import { XPackInfoProvider } from 'plugins/xpack_main/services/xpack_info'; import template from './grokdebugger_route.html'; import './directives/grokdebugger'; + routes .when('/dev_tools/grokdebugger', { template: template, + requireUICapability: 'dev_tools.show', resolve: { licenseCheckResults(Private) { const xpackInfo = Private(XPackInfoProvider); diff --git a/x-pack/plugins/infra/public/apps/start_app.tsx b/x-pack/plugins/infra/public/apps/start_app.tsx index c20c423b7006..96cac959ced7 100644 --- a/x-pack/plugins/infra/public/apps/start_app.tsx +++ b/x-pack/plugins/infra/public/apps/start_app.tsx @@ -14,6 +14,7 @@ import { pluck } from 'rxjs/operators'; // TODO use theme provided from parentApp when kibana supports it import { EuiErrorBoundary } from '@elastic/eui'; +import { UICapabilitiesProvider } from 'ui/capabilities/react'; import { I18nContext } from 'ui/i18n'; import { EuiThemeProvider } from '../../../../common/eui_styled_components'; import { InfraFrontendLibs } from '../lib/lib'; @@ -36,19 +37,21 @@ export async function startApp(libs: InfraFrontendLibs) { return ( - - - - - - - - - - - - - + + + + + + + + + + + + + + + ); }; diff --git a/x-pack/plugins/infra/public/components/source_configuration/fields_configuration_panel.tsx b/x-pack/plugins/infra/public/components/source_configuration/fields_configuration_panel.tsx index fae8a65b3cf6..c48562b355d8 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/fields_configuration_panel.tsx +++ b/x-pack/plugins/infra/public/components/source_configuration/fields_configuration_panel.tsx @@ -14,6 +14,7 @@ interface FieldsConfigurationPanelProps { containerFieldProps: InputFieldProps; hostFieldProps: InputFieldProps; isLoading: boolean; + readOnly: boolean; podFieldProps: InputFieldProps; tiebreakerFieldProps: InputFieldProps; timestampFieldProps: InputFieldProps; @@ -23,6 +24,7 @@ export const FieldsConfigurationPanel = ({ containerFieldProps, hostFieldProps, isLoading, + readOnly, podFieldProps, tiebreakerFieldProps, timestampFieldProps, @@ -57,7 +59,13 @@ export const FieldsConfigurationPanel = ({ /> } > - + @@ -106,7 +115,13 @@ export const FieldsConfigurationPanel = ({ /> } > - + } > - + } > - + ); diff --git a/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx b/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx index 0f0489594d56..3ea0c2ca704c 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx +++ b/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx @@ -12,12 +12,14 @@ import { InputFieldProps } from './source_configuration_form_state'; interface IndicesConfigurationPanelProps { isLoading: boolean; + readOnly: boolean; logAliasFieldProps: InputFieldProps; metricAliasFieldProps: InputFieldProps; } export const IndicesConfigurationPanel = ({ isLoading, + readOnly, logAliasFieldProps, metricAliasFieldProps, }: IndicesConfigurationPanelProps) => ( @@ -55,6 +57,7 @@ export const IndicesConfigurationPanel = ({ data-test-subj="metricIndicesInput" fullWidth disabled={isLoading} + readOnly={readOnly} isLoading={isLoading} {...metricAliasFieldProps} /> @@ -84,6 +87,7 @@ export const IndicesConfigurationPanel = ({ fullWidth disabled={isLoading} isLoading={isLoading} + readOnly={readOnly} {...logAliasFieldProps} /> diff --git a/x-pack/plugins/infra/public/components/source_configuration/name_configuration_panel.tsx b/x-pack/plugins/infra/public/components/source_configuration/name_configuration_panel.tsx index 93f2d03e83cb..99db9c040ed2 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/name_configuration_panel.tsx +++ b/x-pack/plugins/infra/public/components/source_configuration/name_configuration_panel.tsx @@ -12,11 +12,13 @@ import { InputFieldProps } from './source_configuration_form_state'; interface NameConfigurationPanelProps { isLoading: boolean; + readOnly: boolean; nameFieldProps: InputFieldProps; } export const NameConfigurationPanel = ({ isLoading, + readOnly, nameFieldProps, }: NameConfigurationPanelProps) => ( @@ -41,6 +43,7 @@ export const NameConfigurationPanel = ({ data-test-subj="nameInput" fullWidth disabled={isLoading} + readOnly={readOnly} isLoading={isLoading} {...nameFieldProps} /> diff --git a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_flyout.tsx b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_flyout.tsx index 5416d34a208e..d5f3e0f3ada8 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_flyout.tsx +++ b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_flyout.tsx @@ -28,7 +28,14 @@ import { useSourceConfigurationFormState } from './source_configuration_form_sta const noop = () => undefined; -export const SourceConfigurationFlyout: React.FunctionComponent = () => { +interface SourceConfigurationFlyoutProps { + shouldAllowEdit: boolean; +} + +export const SourceConfigurationFlyout: React.FunctionComponent< + SourceConfigurationFlyoutProps +> = props => { + const { shouldAllowEdit } = props; const { isVisible, hide } = useContext(SourceConfigurationFlyoutState.Context); const { @@ -98,18 +105,30 @@ export const SourceConfigurationFlyout: React.FunctionComponent = () => {

- + {shouldAllowEdit ? ( + + ) : ( + + )}

- + @@ -118,6 +137,7 @@ export const SourceConfigurationFlyout: React.FunctionComponent = () => { containerFieldProps={fieldProps.containerField} hostFieldProps={fieldProps.hostField} isLoading={isLoading} + readOnly={!shouldAllowEdit} podFieldProps={fieldProps.podField} tiebreakerFieldProps={fieldProps.tiebreakerField} timestampFieldProps={fieldProps.timestampField} @@ -157,26 +177,28 @@ export const SourceConfigurationFlyout: React.FunctionComponent = () => { )} - - {isLoading ? ( - - Loading - - ) : ( - - - - )} - + {shouldAllowEdit && ( + + {isLoading ? ( + + Loading + + ) : ( + + + + )} + + )} diff --git a/x-pack/plugins/infra/public/components/waffle/node.tsx b/x-pack/plugins/infra/public/components/waffle/node.tsx index 60baefb95144..dfd622732e00 100644 --- a/x-pack/plugins/infra/public/components/waffle/node.tsx +++ b/x-pack/plugins/infra/public/components/waffle/node.tsx @@ -79,6 +79,7 @@ export const Node = injectI18n( > diff --git a/x-pack/plugins/infra/public/components/waffle/node_context_menu.tsx b/x-pack/plugins/infra/public/components/waffle/node_context_menu.tsx index e8918854c234..7e6f62a733d6 100644 --- a/x-pack/plugins/infra/public/components/waffle/node_context_menu.tsx +++ b/x-pack/plugins/infra/public/components/waffle/node_context_menu.tsx @@ -7,7 +7,8 @@ import { EuiContextMenu, EuiContextMenuPanelDescriptor, EuiPopover } from '@elastic/eui'; import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React from 'react'; - +import { UICapabilities } from 'ui/capabilities'; +import { injectUICapabilities } from 'ui/capabilities/react'; import { InfraNodeType, InfraTimerangeInput } from '../../graphql/types'; import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../lib/lib'; import { getNodeDetailUrl, getNodeLogsUrl } from '../../pages/link_to'; @@ -21,89 +22,107 @@ interface Props { isPopoverOpen: boolean; closePopover: () => void; intl: InjectedIntl; + uiCapabilities: UICapabilities; } -export const NodeContextMenu = injectI18n( - ({ options, timeRange, children, node, isPopoverOpen, closePopover, nodeType, intl }: Props) => { - // Due to the changing nature of the fields between APM and this UI, - // We need to have some exceptions until 7.0 & ECS is finalized. Reference - // #26620 for the details for these fields. - // TODO: This is tech debt, remove it after 7.0 & ECS migration. - const APM_FIELDS = { - [InfraNodeType.host]: 'host.hostname', - [InfraNodeType.container]: 'container.id', - [InfraNodeType.pod]: 'kubernetes.pod.uid', - }; +export const NodeContextMenu = injectUICapabilities( + injectI18n( + ({ + options, + timeRange, + children, + node, + isPopoverOpen, + closePopover, + nodeType, + intl, + uiCapabilities, + }: Props) => { + // Due to the changing nature of the fields between APM and this UI, + // We need to have some exceptions until 7.0 & ECS is finalized. Reference + // #26620 for the details for these fields. + // TODO: This is tech debt, remove it after 7.0 & ECS migration. + const APM_FIELDS = { + [InfraNodeType.host]: 'host.hostname', + [InfraNodeType.container]: 'container.id', + [InfraNodeType.pod]: 'kubernetes.pod.uid', + }; + + const nodeLogsUrl = + node.id && uiCapabilities.logs.show + ? getNodeLogsUrl({ + nodeType, + nodeId: node.id, + time: timeRange.to, + }) + : undefined; + const nodeDetailUrl = node.id + ? getNodeDetailUrl({ + nodeType, + nodeId: node.id, + from: timeRange.from, + to: timeRange.to, + }) + : undefined; - const nodeLogsUrl = node.id - ? getNodeLogsUrl({ - nodeType, - nodeId: node.id, - time: timeRange.to, - }) - : undefined; - const nodeDetailUrl = node.id - ? getNodeDetailUrl({ - nodeType, - nodeId: node.id, - from: timeRange.from, - to: timeRange.to, - }) - : undefined; + const apmTracesUrl = uiCapabilities.apm.show + ? { + name: intl.formatMessage( + { + id: 'xpack.infra.nodeContextMenu.viewAPMTraces', + defaultMessage: 'View {nodeType} APM traces', + }, + { nodeType } + ), + href: `../app/apm#/traces?_g=()&kuery=${APM_FIELDS[nodeType]}~20~3A~20~22${node.id}~22`, + 'data-test-subj': 'viewApmTracesContextMenuItem', + } + : undefined; - const apmTracesUrl = { - name: intl.formatMessage( + const panels: EuiContextMenuPanelDescriptor[] = [ { - id: 'xpack.infra.nodeContextMenu.viewAPMTraces', - defaultMessage: 'View {nodeType} APM traces', + id: 0, + title: '', + items: [ + ...(nodeLogsUrl + ? [ + { + name: intl.formatMessage({ + id: 'xpack.infra.nodeContextMenu.viewLogsName', + defaultMessage: 'View logs', + }), + href: nodeLogsUrl, + 'data-test-subj': 'viewLogsContextMenuItem', + }, + ] + : []), + ...(nodeDetailUrl + ? [ + { + name: intl.formatMessage({ + id: 'xpack.infra.nodeContextMenu.viewMetricsName', + defaultMessage: 'View metrics', + }), + href: nodeDetailUrl, + }, + ] + : []), + ...(apmTracesUrl ? [apmTracesUrl] : []), + ], }, - { nodeType } - ), - href: `../app/apm#/traces?_g=()&kuery=${APM_FIELDS[nodeType]}~20~3A~20~22${node.id}~22`, - }; - - const panels: EuiContextMenuPanelDescriptor[] = [ - { - id: 0, - title: '', - items: [ - ...(nodeLogsUrl - ? [ - { - name: intl.formatMessage({ - id: 'xpack.infra.nodeContextMenu.viewLogsName', - defaultMessage: 'View logs', - }), - href: nodeLogsUrl, - }, - ] - : []), - ...(nodeDetailUrl - ? [ - { - name: intl.formatMessage({ - id: 'xpack.infra.nodeContextMenu.viewMetricsName', - defaultMessage: 'View metrics', - }), - href: nodeDetailUrl, - }, - ] - : []), - ...[apmTracesUrl], - ], - }, - ]; + ]; - return ( - - - - ); - } + return ( + + + + ); + } + ) ); diff --git a/x-pack/plugins/infra/public/pages/404.tsx b/x-pack/plugins/infra/public/pages/404.tsx index d99d7339c79d..631c6d8299e0 100644 --- a/x-pack/plugins/infra/public/pages/404.tsx +++ b/x-pack/plugins/infra/public/pages/404.tsx @@ -10,7 +10,7 @@ import React from 'react'; export class NotFoundPage extends React.PureComponent { public render() { return ( -
+
{ - const { show } = useContext(SourceConfigurationFlyoutState.Context); - const { - derivedIndexPattern, - hasFailedLoadingSource, - isLoading, - loadSourceFailureMessage, - loadSource, - metricIndicesExist, - } = useContext(Source.Context); +interface SnapshotPageProps { + intl: InjectedIntl; + uiCapabilities: UICapabilities; +} + +export const SnapshotPage = injectUICapabilities( + injectI18n((props: SnapshotPageProps) => { + const { intl, uiCapabilities } = props; + const { show } = useContext(SourceConfigurationFlyoutState.Context); + const { + derivedIndexPattern, + hasFailedLoadingSource, + isLoading, + loadSourceFailureMessage, + loadSource, + metricIndicesExist, + } = useContext(Source.Context); - return ( - - - intl.formatMessage( + return ( + + + intl.formatMessage( + { + id: 'xpack.infra.infrastructureSnapshotPage.documentTitle', + defaultMessage: '{previousTitle} | Snapshot', + }, + { + previousTitle, + } + ) + } + /> +
-
- - {isLoading ? ( - - ) : metricIndicesExist ? ( - <> - - - - - - - ) : hasFailedLoadingSource ? ( - - ) : ( - - {({ basePath }) => ( - - - - {intl.formatMessage({ - id: 'xpack.infra.homePage.noMetricsIndicesInstructionsActionLabel', - defaultMessage: 'View setup instructions', - })} - - - - - {intl.formatMessage({ - id: 'xpack.infra.configureSourceActionLabel', - defaultMessage: 'Change source configuration', - })} - - - - } - data-test-subj="noMetricsIndicesPrompt" - /> - )} - - )} - - ); -}); + ]} + /> + + {isLoading ? ( + + ) : metricIndicesExist ? ( + <> + + + + + + + ) : hasFailedLoadingSource ? ( + + ) : ( + + {({ basePath }) => ( + + + + {intl.formatMessage({ + id: 'xpack.infra.homePage.noMetricsIndicesInstructionsActionLabel', + defaultMessage: 'View setup instructions', + })} + + + {uiCapabilities.infrastructure.configureSource ? ( + + + {intl.formatMessage({ + id: 'xpack.infra.configureSourceActionLabel', + defaultMessage: 'Change source configuration', + })} + + + ) : null} + + } + data-test-subj="noMetricsIndicesPrompt" + /> + )} + + )} + + ); + }) +); diff --git a/x-pack/plugins/infra/public/pages/logs/page.tsx b/x-pack/plugins/infra/public/pages/logs/page.tsx index 8090316d98d3..d2556bdffe8e 100644 --- a/x-pack/plugins/infra/public/pages/logs/page.tsx +++ b/x-pack/plugins/infra/public/pages/logs/page.tsx @@ -13,7 +13,7 @@ import { LogsPageProviders } from './page_providers'; export const LogsPage = () => ( - + diff --git a/x-pack/plugins/infra/public/pages/logs/page_header.tsx b/x-pack/plugins/infra/public/pages/logs/page_header.tsx index a3acb34bf25c..6f028ea6d643 100644 --- a/x-pack/plugins/infra/public/pages/logs/page_header.tsx +++ b/x-pack/plugins/infra/public/pages/logs/page_header.tsx @@ -4,41 +4,53 @@ * you may not use this file except in compliance with the Elastic License. */ -import { injectI18n } from '@kbn/i18n/react'; +import { injectI18n, InjectedIntl } from '@kbn/i18n/react'; import React from 'react'; +import { UICapabilities } from 'ui/capabilities'; +import { injectUICapabilities } from 'ui/capabilities/react'; import { DocumentTitle } from '../../components/document_title'; import { Header } from '../../components/header'; import { HelpCenterContent } from '../../components/help_center_content'; import { SourceConfigurationFlyout } from '../../components/source_configuration'; -export const LogsPageHeader = injectI18n(({ intl }) => { - return ( - <> -
- - - - - ); -}); +interface LogsPageHeaderProps { + intl: InjectedIntl; + uiCapabilities: UICapabilities; +} + +export const LogsPageHeader = injectUICapabilities( + injectI18n((props: LogsPageHeaderProps) => { + const { intl, uiCapabilities } = props; + return ( + <> +
+ + + + + ); + }) +); diff --git a/x-pack/plugins/infra/public/pages/logs/page_no_indices_content.tsx b/x-pack/plugins/infra/public/pages/logs/page_no_indices_content.tsx index 44cda7fc97aa..d2482479103e 100644 --- a/x-pack/plugins/infra/public/pages/logs/page_no_indices_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/page_no_indices_content.tsx @@ -5,55 +5,72 @@ */ import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { injectI18n } from '@kbn/i18n/react'; +import { injectI18n, InjectedIntl } from '@kbn/i18n/react'; import React, { useContext } from 'react'; +import { UICapabilities } from 'ui/capabilities'; +import { injectUICapabilities } from 'ui/capabilities/react'; import { NoIndices } from '../../components/empty_states/no_indices'; import { SourceConfigurationFlyoutState } from '../../components/source_configuration'; import { WithKibanaChrome } from '../../containers/with_kibana_chrome'; -export const LogsPageNoIndicesContent = injectI18n(({ intl }) => { - const { show } = useContext(SourceConfigurationFlyoutState.Context); +interface LogsPageNoIndicesContentProps { + intl: InjectedIntl; + uiCapabilities: UICapabilities; +} - return ( - - {({ basePath }) => ( - - - - {intl.formatMessage({ - id: 'xpack.infra.logsPage.noLoggingIndicesInstructionsActionLabel', - defaultMessage: 'View setup instructions', - })} - - - - - {intl.formatMessage({ - id: 'xpack.infra.configureSourceActionLabel', - defaultMessage: 'Change source configuration', - })} - - - - } - /> - )} - - ); -}); +export const LogsPageNoIndicesContent = injectUICapabilities( + injectI18n((props: LogsPageNoIndicesContentProps) => { + const { intl, uiCapabilities } = props; + const { show } = useContext(SourceConfigurationFlyoutState.Context); + + return ( + + {({ basePath }) => ( + + + + {intl.formatMessage({ + id: 'xpack.infra.logsPage.noLoggingIndicesInstructionsActionLabel', + defaultMessage: 'View setup instructions', + })} + + + {uiCapabilities.logs.configureSource ? ( + + + {intl.formatMessage({ + id: 'xpack.infra.configureSourceActionLabel', + defaultMessage: 'Change source configuration', + })} + + + ) : null} + + } + /> + )} + + ); + }) +); diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx index 2d5ac5b1c192..eeb58369ff9a 100644 --- a/x-pack/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx @@ -16,7 +16,8 @@ import { import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import { GraphQLFormattedError } from 'graphql'; import React from 'react'; - +import { UICapabilities } from 'ui/capabilities'; +import { injectUICapabilities } from 'ui/capabilities/react'; import euiStyled, { EuiTheme, withTheme } from '../../../../../common/eui_styled_components'; import { InfraMetricsErrorCodes } from '../../../common/errors'; import { AutoSizer } from '../../components/auto_sizer'; @@ -59,200 +60,207 @@ interface Props { }; }; intl: InjectedIntl; + uiCapabilities: UICapabilities; } -export const MetricDetail = withTheme( - injectI18n( - class extends React.PureComponent { - public static displayName = 'MetricDetailPage'; +export const MetricDetail = injectUICapabilities( + withTheme( + injectI18n( + class extends React.PureComponent { + public static displayName = 'MetricDetailPage'; - public render() { - const { intl } = this.props; - const nodeId = this.props.match.params.node; - const nodeType = this.props.match.params.type as InfraNodeType; - const layoutCreator = layoutCreators[nodeType]; - if (!layoutCreator) { - return ( - - ); - } - const layouts = layoutCreator(this.props.theme); + public render() { + const { intl, uiCapabilities } = this.props; + const nodeId = this.props.match.params.node; + const nodeType = this.props.match.params.type as InfraNodeType; + const layoutCreator = layoutCreators[nodeType]; + if (!layoutCreator) { + return ( + + ); + } + const layouts = layoutCreator(this.props.theme); - return ( - - - {({ sourceId }) => ( - - {({ - timeRange, - setTimeRange, - refreshInterval, - setRefreshInterval, - isAutoReloading, - setAutoReload, - }) => ( - - {({ name, filteredLayouts, loading: metadataLoading }) => { - const breadcrumbs = [ - { - href: '#/', - text: intl.formatMessage({ - id: 'xpack.infra.header.infrastructureTitle', - defaultMessage: 'Infrastructure', - }), - }, - { text: name }, - ]; - return ( - -
- - - + + {({ sourceId }) => ( + + {({ + timeRange, + setTimeRange, + refreshInterval, + setRefreshInterval, + isAutoReloading, + setAutoReload, + }) => ( + + {({ name, filteredLayouts, loading: metadataLoading }) => { + const breadcrumbs = [ + { + href: '#/', + text: intl.formatMessage({ + id: 'xpack.infra.header.infrastructureTitle', + defaultMessage: 'Infrastructure', + }), + }, + { text: name }, + ]; + return ( + +
+ - - - {({ metrics, error, loading, refetch }) => { - if (error) { - const invalidNodeError = error.graphQLErrors.some( - (err: GraphQLFormattedError) => - err.code === InfraMetricsErrorCodes.invalid_node - ); + /> + + + + + {({ metrics, error, loading, refetch }) => { + if (error) { + const invalidNodeError = error.graphQLErrors.some( + (err: GraphQLFormattedError) => + err.code === InfraMetricsErrorCodes.invalid_node + ); + return ( + <> + + intl.formatMessage( + { + id: + 'xpack.infra.metricDetailPage.documentTitleError', + defaultMessage: '{previousTitle} | Uh oh', + }, + { + previousTitle, + } + ) + } + /> + {invalidNodeError ? ( + + ) : ( + + )} + + ); + } return ( - <> - - intl.formatMessage( - { - id: - 'xpack.infra.metricDetailPage.documentTitleError', - defaultMessage: '{previousTitle} | Uh oh', - }, - { - previousTitle, - } - ) - } + + - {invalidNodeError ? ( - - ) : ( - - )} - + + {({ measureRef, bounds: { width = 0 } }) => { + return ( + + + + + + + +

{name}

+
+
+ +
+
+
+ + + 0 && isAutoReloading + ? false + : loading + } + refetch={refetch} + onChangeRangeTime={setTimeRange} + isLiveStreaming={isAutoReloading} + stopLiveStreaming={() => setAutoReload(false)} + /> + +
+
+ ); + }} +
+
); - } - return ( - - - - {({ measureRef, bounds: { width = 0 } }) => { - return ( - - - - - - - -

{name}

-
-
- -
-
-
+ }} +
+
+ + ); + }} + + )} + + )} + + + ); + } - - 0 && isAutoReloading - ? false - : loading - } - refetch={refetch} - onChangeRangeTime={setTimeRange} - isLiveStreaming={isAutoReloading} - stopLiveStreaming={() => setAutoReload(false)} - /> - - - - ); - }} - - - ); - }} -
-
- - ); - }} - - )} - - )} - - - ); + private handleClick = (section: InfraMetricLayoutSection) => () => { + const id = section.linkToId || section.id; + const el = document.getElementById(id); + if (el) { + el.scrollIntoView(); + } + }; } - - private handleClick = (section: InfraMetricLayoutSection) => () => { - const id = section.linkToId || section.id; - const el = document.getElementById(id); - if (el) { - el.scrollIntoView(); - } - }; - } + ) ) ); diff --git a/x-pack/plugins/infra/public/routes.tsx b/x-pack/plugins/infra/public/routes.tsx index 12328f8c601a..5a89d73bc278 100644 --- a/x-pack/plugins/infra/public/routes.tsx +++ b/x-pack/plugins/infra/public/routes.tsx @@ -8,6 +8,8 @@ import { History } from 'history'; import React from 'react'; import { Redirect, Route, Router, Switch } from 'react-router-dom'; +import { UICapabilities } from 'ui/capabilities'; +import { injectUICapabilities } from 'ui/capabilities/react'; import { NotFoundPage } from './pages/404'; import { InfrastructurePage } from './pages/infrastructure'; import { LinkToPage } from './pages/link_to'; @@ -16,21 +18,34 @@ import { MetricDetail } from './pages/metrics'; interface RouterProps { history: History; + uiCapabilities: UICapabilities; } -export const PageRouter: React.SFC = ({ history }) => { +const PageRouterComponent: React.SFC = ({ history, uiCapabilities }) => { return ( - - - - - + {uiCapabilities.infrastructure.show && ( + + )} + {uiCapabilities.infrastructure.show && ( + + )} + {uiCapabilities.infrastructure.show && ( + + )} + {uiCapabilities.logs.show && } + {uiCapabilities.infrastructure.show && ( + + )} - + {uiCapabilities.infrastructure.show && ( + + )} ); }; + +export const PageRouter = injectUICapabilities(PageRouterComponent); diff --git a/x-pack/plugins/infra/server/kibana.index.ts b/x-pack/plugins/infra/server/kibana.index.ts index e463e8dcb595..20480aa93b31 100644 --- a/x-pack/plugins/infra/server/kibana.index.ts +++ b/x-pack/plugins/infra/server/kibana.index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import { Server } from 'hapi'; import JoiNamespace from 'joi'; import { initInfraServer } from './infra_server'; @@ -20,6 +21,65 @@ export const initServerWithKibana = (kbnServer: KbnServer) => { // Register a function with server to manage the collection of usage stats kbnServer.usage.collectorSet.register(UsageCollector.getUsageCollector(kbnServer)); + + const xpackMainPlugin = kbnServer.plugins.xpack_main; + xpackMainPlugin.registerFeature({ + id: 'infrastructure', + name: i18n.translate('xpack.infra.featureRegistry.linkInfrastructureTitle', { + defaultMessage: 'Infrastructure', + }), + icon: 'infraApp', + navLinkId: 'infra:home', + app: ['infra', 'kibana'], + catalogue: ['infraops'], + privileges: { + all: { + api: ['infra'], + savedObject: { + all: ['infrastructure-ui-source'], + read: ['config'], + }, + ui: ['show', 'configureSource'], + }, + read: { + api: ['infra'], + savedObject: { + all: [], + read: ['config', 'infrastructure-ui-source'], + }, + ui: ['show'], + }, + }, + }); + + xpackMainPlugin.registerFeature({ + id: 'logs', + name: i18n.translate('xpack.infra.featureRegistry.linkLogsTitle', { + defaultMessage: 'Logs', + }), + icon: 'loggingApp', + navLinkId: 'infra:logs', + app: ['infra', 'kibana'], + catalogue: ['infralogging'], + privileges: { + all: { + api: ['infra'], + savedObject: { + all: ['infrastructure-ui-source'], + read: ['config'], + }, + ui: ['show', 'configureSource'], + }, + read: { + api: ['infra'], + savedObject: { + all: [], + read: ['config', 'infrastructure-ui-source'], + }, + ui: ['show'], + }, + }, + }); }; export const getConfigSchema = (Joi: typeof JoiNamespace) => { diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts index 8b13cea43065..ae276229b30f 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -56,6 +56,9 @@ export class InfraKibanaBackendFrameworkAdapter implements InfraBackendFramework schema, }), path: routePath, + route: { + tags: ['access:infra'], + }, }, plugin: graphqlHapi, }); @@ -67,6 +70,9 @@ export class InfraKibanaBackendFrameworkAdapter implements InfraBackendFramework passHeader: `'kbn-version': '${this.version}'`, }), path: `${routePath}/graphiql`, + route: { + tags: ['access:infra'], + }, }, plugin: graphiqlHapi, }); diff --git a/x-pack/plugins/maps/index.js b/x-pack/plugins/maps/index.js index 6bd0591f1ac6..2e97a184e580 100644 --- a/x-pack/plugins/maps/index.js +++ b/x-pack/plugins/maps/index.js @@ -79,6 +79,33 @@ export function maps(kibana) { const xpackMainPlugin = server.plugins.xpack_main; let routesInitialized = false; + xpackMainPlugin.registerFeature({ + id: 'maps', + name: i18n.translate('xpack.maps.featureRegistry.mapsFeatureName', { + defaultMessage: 'Maps', + }), + icon: APP_ICON, + navLinkId: 'maps', + app: [APP_ID, 'kibana'], + catalogue: ['maps'], + privileges: { + all: { + savedObject: { + all: ['map'], + read: ['config', 'index-pattern'] + }, + ui: ['save'], + }, + read: { + savedObject: { + all: [], + read: ['map', 'config', 'index-pattern'] + }, + ui: [], + }, + } + }); + watchStatusAndLicenseToInitialize(xpackMainPlugin, this, async license => { if (license && license.maps && !routesInitialized) { diff --git a/x-pack/plugins/maps/public/angular/listing_ng_wrapper.html b/x-pack/plugins/maps/public/angular/listing_ng_wrapper.html index 38c3a2b5c69f..ed787edec6e0 100644 --- a/x-pack/plugins/maps/public/angular/listing_ng_wrapper.html +++ b/x-pack/plugins/maps/public/angular/listing_ng_wrapper.html @@ -2,4 +2,5 @@ find="find" delete="delete" listing-limit="listingLimit" + read-only="readOnly" /> diff --git a/x-pack/plugins/maps/public/angular/map_controller.js b/x-pack/plugins/maps/public/angular/map_controller.js index c611cd8f0c27..00b5a1005f6f 100644 --- a/x-pack/plugins/maps/public/angular/map_controller.js +++ b/x-pack/plugins/maps/public/angular/map_controller.js @@ -10,6 +10,7 @@ import 'ui/listen'; import React from 'react'; import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { uiCapabilities } from 'ui/capabilities'; import { render, unmountComponentAtNode } from 'react-dom'; import { uiModules } from 'ui/modules'; import { timefilter } from 'ui/timefilter'; @@ -31,6 +32,7 @@ import { getIsFullScreen, updateFlyout, FLYOUT_STATE, + setReadOnly, setIsLayerTOCOpen } from '../store/ui'; import { getUniqueIndexPatternIds } from '../selectors/map_selectors'; @@ -125,6 +127,7 @@ app.controller('GisMapController', ($scope, $route, config, kbnUrl, localStorage // clear old UI state store.dispatch(setSelectedLayer(null)); store.dispatch(updateFlyout(FLYOUT_STATE.NONE)); + store.dispatch(setReadOnly(!uiCapabilities.maps.save)); handleStoreChanges(store); unsubscribe = store.subscribe(() => { @@ -294,7 +297,7 @@ app.controller('GisMapController', ($scope, $route, config, kbnUrl, localStorage const inspectorAdapters = getInspectorAdapters(store.getState()); Inspector.open(inspectorAdapters, {}); } - }, { + }, ...(uiCapabilities.maps.save ? [{ key: i18n.translate('xpack.maps.mapController.saveMapButtonLabel', { defaultMessage: `save` }), @@ -331,5 +334,7 @@ app.controller('GisMapController', ($scope, $route, config, kbnUrl, localStorage />); showSaveModal(saveModal); } - }]; + }] : []) + ]; }); + diff --git a/x-pack/plugins/maps/public/index.js b/x-pack/plugins/maps/public/index.js index e77a4b30a22e..532bbd09dbd8 100644 --- a/x-pack/plugins/maps/public/index.js +++ b/x-pack/plugins/maps/public/index.js @@ -14,6 +14,7 @@ import 'uiExports/search'; import 'uiExports/embeddableFactories'; import 'ui/agg_types'; +import { uiCapabilities } from 'ui/capabilities'; import chrome from 'ui/chrome'; import routes from 'ui/routes'; import 'ui/kbn_top_nav'; @@ -52,6 +53,7 @@ routes $scope.delete = (ids) => { return gisMapSavedObjectLoader.delete(ids); }; + $scope.readOnly = !uiCapabilities.maps.save; }, resolve: { hasMaps: function (kbnUrl) { diff --git a/x-pack/plugins/maps/public/shared/components/map_listing.js b/x-pack/plugins/maps/public/shared/components/map_listing.js index 2d99eda19132..0a4accf030a8 100644 --- a/x-pack/plugins/maps/public/shared/components/map_listing.js +++ b/x-pack/plugins/maps/public/shared/components/map_listing.js @@ -331,13 +331,18 @@ export class MapListing extends React.Component { totalItemCount: this.state.items.length, pageSizeOptions: [10, 20, 50], }; - const selection = { - onSelectionChange: (selection) => { - this.setState({ - selectedIds: selection.map(item => { return item.id; }) - }); - } - }; + + let selection = false; + if (!this.props.readOnly) { + selection = { + onSelectionChange: (selection) => { + this.setState({ + selectedIds: selection.map(item => { return item.id; }) + }); + } + }; + } + const sorting = {}; if (this.state.sortField) { sorting.sort = { @@ -364,7 +369,7 @@ export class MapListing extends React.Component { renderListing() { let createButton; - if (!this.props.hideWriteControls) { + if (!this.props.readOnly) { createButton = ( { xpackMainPlugin.info.feature(thisPlugin.id).registerLicenseCheckResultsGenerator(checkLicense); }); + xpackMainPlugin.registerFeature({ + id: 'ml', + name: i18n.translate('xpack.ml.featureRegistry.mlFeatureName', { + defaultMessage: 'Machine Learning', + }), + icon: 'machineLearningApp', + navLinkId: 'ml', + app: ['ml', 'kibana'], + catalogue: ['ml'], + privileges: {}, + reserved: { + privilege: { + savedObject: { + all: [], + read: ['config'] + }, + ui: [], + }, + description: i18n.translate('xpack.ml.feature.reserved.description', { + defaultMessage: 'To grant users access, you should also assign either the machine_learning_user or machine_learning_admin role.' + }) + } + }); + // Add server routes and initialize the plugin here const commonRouteConfig = { pre: [ diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js b/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js index ce94e47203b2..76fd0675fb41 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js @@ -406,7 +406,7 @@ export class JobsListView extends Component { -
+
diff --git a/x-pack/plugins/monitoring/init.js b/x-pack/plugins/monitoring/init.js index b85a402af5ed..358fd715d7df 100644 --- a/x-pack/plugins/monitoring/init.js +++ b/x-pack/plugins/monitoring/init.js @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import { LOGGING_TAG, KIBANA_MONITORING_LOGGING_TAG } from './common/constants'; import { requireUIRoutes } from './server/routes'; import { instantiateClient } from './server/es_client/instantiate_client'; @@ -54,6 +55,30 @@ export const init = (monitoringPlugin, server) => { } }); + xpackMainPlugin.registerFeature({ + id: 'monitoring', + name: i18n.translate('xpack.monitoring.featureRegistry.monitoringFeatureName', { + defaultMessage: 'Stack Monitoring', + }), + icon: 'monitoringApp', + navLinkId: 'monitoring', + app: ['monitoring', 'kibana'], + catalogue: ['monitoring'], + privileges: {}, + reserved: { + privilege: { + savedObject: { + all: [], + read: ['config'] + }, + ui: [], + }, + description: i18n.translate('xpack.monitoring.feature.reserved.description', { + defaultMessage: 'To grant users access, you should also assign the monitoring_user role.' + }) + } + }); + const bulkUploader = initBulkUploader(kbnServer, server); const kibanaCollectionEnabled = config.get('xpack.monitoring.kibana.collection.enabled'); const { info: xpackMainInfo } = xpackMainPlugin; diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_list.test.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_list.test.js index dd3044dc491b..4f1ddb4a9a81 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_list.test.js +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_list.test.js @@ -14,6 +14,16 @@ import { getRemoteClusterMock } from '../../fixtures/remote_cluster'; jest.mock('ui/chrome', () => ({ addBasePath: (path) => path || '/api/remote_clusters', breadcrumbs: { set: () => {} }, + getInjected: (key) => { + if (key === 'uiCapabilities') { + return { + navLinks: {}, + management: {}, + catalogue: {} + }; + } + throw new Error(`Unexpected call to chrome.getInjected with key ${key}`); + } })); const testBedOptions = { diff --git a/x-pack/plugins/rollup/__jest__/client_integration/job_create_date_histogram.test.js b/x-pack/plugins/rollup/__jest__/client_integration/job_create_date_histogram.test.js index 1bf2640590cd..42e2d6224bdc 100644 --- a/x-pack/plugins/rollup/__jest__/client_integration/job_create_date_histogram.test.js +++ b/x-pack/plugins/rollup/__jest__/client_integration/job_create_date_histogram.test.js @@ -16,6 +16,7 @@ jest.mock('ui/index_patterns', () => { jest.mock('ui/chrome', () => ({ addBasePath: () => '/api/rollup', breadcrumbs: { set: () => {} }, + getInjected: () => ({}), })); jest.mock('lodash/function/debounce', () => fn => fn); diff --git a/x-pack/plugins/rollup/__jest__/client_integration/job_create_histogram.test.js b/x-pack/plugins/rollup/__jest__/client_integration/job_create_histogram.test.js index 132a2faf8b96..e9b8ea94993a 100644 --- a/x-pack/plugins/rollup/__jest__/client_integration/job_create_histogram.test.js +++ b/x-pack/plugins/rollup/__jest__/client_integration/job_create_histogram.test.js @@ -15,6 +15,7 @@ jest.mock('ui/index_patterns', () => { jest.mock('ui/chrome', () => ({ addBasePath: () => '/api/rollup', breadcrumbs: { set: () => {} }, + getInjected: () => ({}), })); jest.mock('lodash/function/debounce', () => fn => fn); diff --git a/x-pack/plugins/rollup/__jest__/client_integration/job_create_logistics.test.js b/x-pack/plugins/rollup/__jest__/client_integration/job_create_logistics.test.js index 1331bd15ffe2..f125ffb0b8ad 100644 --- a/x-pack/plugins/rollup/__jest__/client_integration/job_create_logistics.test.js +++ b/x-pack/plugins/rollup/__jest__/client_integration/job_create_logistics.test.js @@ -17,6 +17,7 @@ jest.mock('ui/index_patterns', () => { jest.mock('ui/chrome', () => ({ addBasePath: () => '/api/rollup', breadcrumbs: { set: () => {} }, + getInjected: () => ({}), })); jest.mock('lodash/function/debounce', () => fn => fn); diff --git a/x-pack/plugins/rollup/__jest__/client_integration/job_create_metrics.test.js b/x-pack/plugins/rollup/__jest__/client_integration/job_create_metrics.test.js index 30923cd8ab73..67beb861ea92 100644 --- a/x-pack/plugins/rollup/__jest__/client_integration/job_create_metrics.test.js +++ b/x-pack/plugins/rollup/__jest__/client_integration/job_create_metrics.test.js @@ -15,6 +15,7 @@ jest.mock('ui/index_patterns', () => { jest.mock('ui/chrome', () => ({ addBasePath: () => '/api/rollup', breadcrumbs: { set: () => {} }, + getInjected: () => ({}), })); jest.mock('lodash/function/debounce', () => fn => fn); diff --git a/x-pack/plugins/rollup/__jest__/client_integration/job_create_review.test.js b/x-pack/plugins/rollup/__jest__/client_integration/job_create_review.test.js index 4810a2dcae20..c6629640e199 100644 --- a/x-pack/plugins/rollup/__jest__/client_integration/job_create_review.test.js +++ b/x-pack/plugins/rollup/__jest__/client_integration/job_create_review.test.js @@ -15,6 +15,7 @@ jest.mock('ui/index_patterns', () => { jest.mock('ui/chrome', () => ({ addBasePath: (path) => path, breadcrumbs: { set: () => {} }, + getInjected: () => ({}), })); jest.mock('lodash/function/debounce', () => fn => fn); diff --git a/x-pack/plugins/rollup/__jest__/client_integration/job_create_terms.test.js b/x-pack/plugins/rollup/__jest__/client_integration/job_create_terms.test.js index ef16864bece9..cad80f8eef3b 100644 --- a/x-pack/plugins/rollup/__jest__/client_integration/job_create_terms.test.js +++ b/x-pack/plugins/rollup/__jest__/client_integration/job_create_terms.test.js @@ -15,6 +15,7 @@ jest.mock('ui/index_patterns', () => { jest.mock('ui/chrome', () => ({ addBasePath: () => '/api/rollup', breadcrumbs: { set: () => {} }, + getInjected: () => ({}), })); jest.mock('lodash/function/debounce', () => fn => fn); diff --git a/x-pack/plugins/rollup/__jest__/client_integration/job_list.test.js b/x-pack/plugins/rollup/__jest__/client_integration/job_list.test.js index ec7977ff58fa..5af5be2997b4 100644 --- a/x-pack/plugins/rollup/__jest__/client_integration/job_list.test.js +++ b/x-pack/plugins/rollup/__jest__/client_integration/job_list.test.js @@ -18,6 +18,16 @@ setHttp(axios.create()); jest.mock('ui/chrome', () => ({ addBasePath: (path) => path ? path : 'api/rollup', breadcrumbs: { set: () => {} }, + getInjected: (key) => { + if (key === 'uiCapabilities') { + return { + navLinks: {}, + management: {}, + catalogue: {} + }; + } + throw new Error(`Unexpected call to chrome.getInjected with key ${key}`); + } })); jest.mock('../../public/crud_app/services', () => { diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.test.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.test.js index 8b220027a75f..c883b8065cd0 100644 --- a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.test.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.test.js @@ -11,6 +11,15 @@ import { JobList } from './job_list'; jest.mock('ui/chrome', () => ({ addBasePath: () => {}, breadcrumbs: { set: () => {} }, + getInjected: (key) => { + if (key === 'uiCapabilities') { + return { + navLinks: {}, + management: {}, + catalogue: {} + }; + } + } })); jest.mock('../../services', () => { diff --git a/x-pack/plugins/searchprofiler/public/app.js b/x-pack/plugins/searchprofiler/public/app.js index e6510a1b28cb..442aa3d7070d 100644 --- a/x-pack/plugins/searchprofiler/public/app.js +++ b/x-pack/plugins/searchprofiler/public/app.js @@ -8,6 +8,7 @@ // K5 imports import { uiModules } from 'ui/modules'; import uiRoutes from 'ui/routes'; +import 'ui/capabilities/route_setup'; import { notify } from 'ui/notify'; // License @@ -31,6 +32,7 @@ import { defaultQuery } from './templates/default_query'; uiRoutes.when('/dev_tools/searchprofiler', { template: template, + requireUICapability: 'dev_tools.show', controller: ($scope, i18n) => { $scope.registerLicenseLinkLabel = i18n('xpack.searchProfiler.registerLicenseLinkLabel', { defaultMessage: 'register a license' }); diff --git a/x-pack/plugins/searchprofiler/public/templates/index.html b/x-pack/plugins/searchprofiler/public/templates/index.html index dbfa9aa1a4a7..10c7b045bb40 100644 --- a/x-pack/plugins/searchprofiler/public/templates/index.html +++ b/x-pack/plugins/searchprofiler/public/templates/index.html @@ -1,4 +1,4 @@ - +
diff --git a/x-pack/plugins/security/common/constants.js b/x-pack/plugins/security/common/constants.ts similarity index 77% rename from x-pack/plugins/security/common/constants.js rename to x-pack/plugins/security/common/constants.ts index 5fb316b77250..bca0684209ba 100644 --- a/x-pack/plugins/security/common/constants.js +++ b/x-pack/plugins/security/common/constants.ts @@ -7,3 +7,5 @@ export const GLOBAL_RESOURCE = '*'; export const IGNORED_TYPES = ['space']; export const REALMS_ELIGIBLE_FOR_PASSWORD_CHANGE = ['reserved', 'native']; +export const APPLICATION_PREFIX = 'kibana-'; +export const RESERVED_PRIVILEGES_APPLICATION_WILDCARD = 'kibana-*'; diff --git a/x-pack/plugins/security/common/model/index_privilege.ts b/x-pack/plugins/security/common/model/features_privileges.ts similarity index 63% rename from x-pack/plugins/security/common/model/index_privilege.ts rename to x-pack/plugins/security/common/model/features_privileges.ts index 560e8df5e126..09ed7d7934be 100644 --- a/x-pack/plugins/security/common/model/index_privilege.ts +++ b/x-pack/plugins/security/common/model/features_privileges.ts @@ -4,11 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export interface IndexPrivilege { - names: string[]; - privileges: string[]; - field_security?: { - grant?: string[]; - }; - query?: string; +export interface FeaturesPrivileges { + [featureId: string]: string[]; } diff --git a/x-pack/plugins/security/common/model/index.ts b/x-pack/plugins/security/common/model/index.ts new file mode 100644 index 000000000000..17def6cd4c1a --- /dev/null +++ b/x-pack/plugins/security/common/model/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 { Role, RoleIndexPrivilege, RoleKibanaPrivilege } from './role'; +export { FeaturesPrivileges } from './features_privileges'; +export { RawKibanaPrivileges, RawKibanaFeaturePrivileges } from './raw_kibana_privileges'; +export { KibanaPrivileges } from './kibana_privileges'; diff --git a/x-pack/plugins/security/common/model/kibana_privileges/feature_privileges.ts b/x-pack/plugins/security/common/model/kibana_privileges/feature_privileges.ts new file mode 100644 index 000000000000..fd4cdf33028e --- /dev/null +++ b/x-pack/plugins/security/common/model/kibana_privileges/feature_privileges.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { FeaturesPrivileges } from '../features_privileges'; +import { RawKibanaFeaturePrivileges } from '../raw_kibana_privileges'; + +export class KibanaFeaturePrivileges { + constructor(private readonly featurePrivilegesMap: RawKibanaFeaturePrivileges) {} + + public getAllPrivileges(): FeaturesPrivileges { + return Object.entries(this.featurePrivilegesMap).reduce((acc, [featureId, privileges]) => { + return { + ...acc, + [featureId]: Object.keys(privileges), + }; + }, {}); + } + + public getPrivileges(featureId: string): string[] { + const featurePrivileges = this.featurePrivilegesMap[featureId]; + if (featurePrivileges == null) { + return []; + } + + return Object.keys(featurePrivileges); + } + + public getActions(featureId: string, privilege: string): string[] { + if (!this.featurePrivilegesMap[featureId]) { + return []; + } + return this.featurePrivilegesMap[featureId][privilege] || []; + } +} diff --git a/x-pack/plugins/security/common/model/kibana_privileges/global_privileges.ts b/x-pack/plugins/security/common/model/kibana_privileges/global_privileges.ts new file mode 100644 index 000000000000..ffe55b813217 --- /dev/null +++ b/x-pack/plugins/security/common/model/kibana_privileges/global_privileges.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export class KibanaGlobalPrivileges { + constructor(private readonly globalPrivilegesMap: Record) {} + + public getAllPrivileges(): string[] { + return Object.keys(this.globalPrivilegesMap); + } + + public getActions(privilege: string): string[] { + return this.globalPrivilegesMap[privilege] || []; + } +} diff --git a/x-pack/plugins/security/common/model/kibana_privileges/index.ts b/x-pack/plugins/security/common/model/kibana_privileges/index.ts new file mode 100644 index 000000000000..ab9baa1356c4 --- /dev/null +++ b/x-pack/plugins/security/common/model/kibana_privileges/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 { KibanaPrivileges } from './kibana_privileges'; diff --git a/x-pack/plugins/security/common/model/kibana_privileges/kibana_privileges.ts b/x-pack/plugins/security/common/model/kibana_privileges/kibana_privileges.ts new file mode 100644 index 000000000000..61e5f083a779 --- /dev/null +++ b/x-pack/plugins/security/common/model/kibana_privileges/kibana_privileges.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RawKibanaPrivileges } from '../raw_kibana_privileges'; +import { KibanaFeaturePrivileges } from './feature_privileges'; +import { KibanaGlobalPrivileges } from './global_privileges'; +import { KibanaSpacesPrivileges } from './spaces_privileges'; + +export class KibanaPrivileges { + constructor(private readonly rawKibanaPrivileges: RawKibanaPrivileges) {} + + public getGlobalPrivileges() { + return new KibanaGlobalPrivileges(this.rawKibanaPrivileges.global); + } + + public getSpacesPrivileges() { + return new KibanaSpacesPrivileges(this.rawKibanaPrivileges.space); + } + + public getFeaturePrivileges() { + return new KibanaFeaturePrivileges(this.rawKibanaPrivileges.features); + } +} diff --git a/x-pack/plugins/security/common/model/kibana_privileges/spaces_privileges.ts b/x-pack/plugins/security/common/model/kibana_privileges/spaces_privileges.ts new file mode 100644 index 000000000000..5c8b4196a2b5 --- /dev/null +++ b/x-pack/plugins/security/common/model/kibana_privileges/spaces_privileges.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export class KibanaSpacesPrivileges { + constructor(private readonly spacesPrivilegesMap: Record) {} + + public getAllPrivileges(): string[] { + return Object.keys(this.spacesPrivilegesMap); + } + + public getActions(privilege: string): string[] { + return this.spacesPrivilegesMap[privilege] || []; + } +} diff --git a/x-pack/plugins/security/common/model/raw_kibana_privileges.ts b/x-pack/plugins/security/common/model/raw_kibana_privileges.ts new file mode 100644 index 000000000000..1b1584a4ce58 --- /dev/null +++ b/x-pack/plugins/security/common/model/raw_kibana_privileges.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface RawKibanaFeaturePrivileges { + [featureId: string]: { + [privilegeId: string]: string[]; + }; +} + +export interface RawKibanaPrivileges { + global: Record; + features: RawKibanaFeaturePrivileges; + space: Record; + reserved: Record; +} diff --git a/x-pack/plugins/security/common/model/role.ts b/x-pack/plugins/security/common/model/role.ts index 5b1094c8c3a0..19bea7ccdfef 100644 --- a/x-pack/plugins/security/common/model/role.ts +++ b/x-pack/plugins/security/common/model/role.ts @@ -4,26 +4,38 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IndexPrivilege } from './index_privilege'; -import { KibanaPrivilege } from './kibana_privilege'; +import { FeaturesPrivileges } from './features_privileges'; + +export interface RoleIndexPrivilege { + names: string[]; + privileges: string[]; + field_security?: { + grant?: string[]; + }; + query?: string; +} + +export interface RoleKibanaPrivilege { + spaces: string[]; + base: string[]; + feature: FeaturesPrivileges; + _reserved?: string[]; +} export interface Role { name: string; elasticsearch: { cluster: string[]; - indices: IndexPrivilege[]; + indices: RoleIndexPrivilege[]; run_as: string[]; }; - kibana: { - global: KibanaPrivilege[]; - space: { - [spaceId: string]: KibanaPrivilege[]; - }; - }; + kibana: RoleKibanaPrivilege[]; metadata?: { [anyKey: string]: any; }; transient_metadata?: { [anyKey: string]: any; }; + _transform_error?: string[]; + _unrecognized_applications?: string[]; } diff --git a/x-pack/plugins/security/common/privilege_calculator_utils.test.ts b/x-pack/plugins/security/common/privilege_calculator_utils.test.ts new file mode 100644 index 000000000000..7a70750656f1 --- /dev/null +++ b/x-pack/plugins/security/common/privilege_calculator_utils.test.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { areActionsFullyCovered, compareActions } from './privilege_calculator_utils'; + +describe('#compareActions', () => { + it(`returns -1 when the first action set is more permissive than the second action set`, () => { + const actionSet1 = ['foo:/*', 'bar']; + const actionSet2 = ['foo:/*']; + expect(compareActions(actionSet1, actionSet2)).toEqual(-1); + }); + + it(`returns 1 when the second action set is more permissive than the first action set`, () => { + const actionSet1 = ['foo:/*']; + const actionSet2 = ['foo:/*', 'bar']; + expect(compareActions(actionSet1, actionSet2)).toEqual(1); + }); + + it('works without wildcards', () => { + const actionSet1 = ['foo:/bar', 'foo:/bar/baz', 'login', 'somethingElse']; + const actionSet2 = ['foo:/bar', 'foo:/bar/baz', 'login']; + expect(compareActions(actionSet1, actionSet2)).toEqual(-1); + }); + + it('handles wildcards correctly', () => { + const actionSet1 = ['foo:/bar/*']; + const actionSet2 = ['foo:/bar/bam', 'foo:/bar/baz/*']; + expect(compareActions(actionSet1, actionSet2)).toEqual(-1); + }); + + it('supports ties in a stable-sort order', () => { + const actionSet1 = ['foo:/bar/bam', 'foo:/bar/baz/*']; + const actionSet2 = ['foo:/bar/bam', 'foo:/bar/baz/*']; + expect(compareActions(actionSet1, actionSet2)).toEqual(-1); + }); + + it('does not support actions where one is not a subset of the other', () => { + const actionSet1 = ['foo:/bar/bam', 'foo:/bar/baz/*']; + const actionSet2 = ['bar:/*']; + + // check both directions + expect(() => compareActions(actionSet1, actionSet2)).toThrowErrorMatchingInlineSnapshot( + `"Non-comparable action sets! Expected one set of actions to be a subset of the other!"` + ); + expect(() => compareActions(actionSet2, actionSet1)).toThrowErrorMatchingInlineSnapshot( + `"Non-comparable action sets! Expected one set of actions to be a subset of the other!"` + ); + }); +}); + +describe('#areActionsFullyCovered', () => { + it('returns true for two empty sets', () => { + const actionSet1: string[] = []; + const actionSet2: string[] = []; + expect(areActionsFullyCovered(actionSet1, actionSet2)).toEqual(true); + }); + + it('returns true when the first set fully covers the second set', () => { + const actionSet1: string[] = ['foo:/*', 'bar:/*']; + const actionSet2: string[] = ['foo:/bar', 'bar:/baz']; + + expect(areActionsFullyCovered(actionSet1, actionSet2)).toEqual(true); + }); + + it('returns false when the first set does not fully cover the second set', () => { + const actionSet1: string[] = ['foo:/bar', 'bar:/baz']; + const actionSet2: string[] = ['foo:/*', 'bar:/*']; + + expect(areActionsFullyCovered(actionSet1, actionSet2)).toEqual(false); + }); + + it('returns true for ties', () => { + const actionSet1: string[] = ['foo:/bar', 'bar:/baz']; + const actionSet2: string[] = ['foo:/bar', 'bar:/baz']; + + expect(areActionsFullyCovered(actionSet1, actionSet2)).toEqual(true); + }); + + it('can handle actions where one is not a subset of the other', () => { + const actionSet1 = ['foo:/bar/bam', 'foo:/bar/baz/*']; + const actionSet2 = ['bar:/*']; + + expect(areActionsFullyCovered(actionSet1, actionSet2)).toEqual(false); + }); +}); diff --git a/x-pack/plugins/security/common/privilege_calculator_utils.ts b/x-pack/plugins/security/common/privilege_calculator_utils.ts new file mode 100644 index 000000000000..e767862c4675 --- /dev/null +++ b/x-pack/plugins/security/common/privilege_calculator_utils.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import _ from 'lodash'; + +/** + * Given two sets of actions, where one set is known to be a subset of the other, this will + * determine which set of actions is most permissive, using standard sorting return values: + * -1: actions1 is most permissive + * 1: actions2 is most permissive + * + * All privileges are hierarchal at this point. + * + * @param actionSet1 + * @param actionSet2 + */ +export function compareActions(actionSet1: string[], actionSet2: string[]) { + if (areActionsFullyCovered(actionSet1, actionSet2)) { + return -1; + } + if (areActionsFullyCovered(actionSet2, actionSet1)) { + return 1; + } + throw new Error( + `Non-comparable action sets! Expected one set of actions to be a subset of the other!` + ); +} +/** + * Given two sets of actions, this will determine if the first set fully covers the second set. + * "fully covers" means that all of the actions granted by the second set are also granted by the first set. + * @param actionSet1 + * @param actionSet2 + */ +export function areActionsFullyCovered(actionSet1: string[], actionSet2: string[]) { + const actionExpressions = actionSet1.map(actionToRegExp); + + const isFullyCovered = actionSet2.every((assigned: string) => + // Does any expression from the first set match this action in the second set? + actionExpressions.some((exp: RegExp) => exp.test(assigned)) + ); + + return isFullyCovered; +} + +function actionToRegExp(action: string) { + // Actions are strings that may or may not end with a wildcard ("*"). + // This will excape all characters in the action string that are not the wildcard character. + // Each wildcard character is then turned into a ".*" before the entire thing is turned into a regexp. + return new RegExp( + action + .split('*') + .map(part => _.escapeRegExp(part)) + .join('.*') + ); +} diff --git a/x-pack/plugins/security/index.js b/x-pack/plugins/security/index.js index 42f8d707922c..c4589cb6b8c6 100644 --- a/x-pack/plugins/security/index.js +++ b/x-pack/plugins/security/index.js @@ -9,6 +9,7 @@ import { getUserProvider } from './server/lib/get_user'; import { initAuthenticateApi } from './server/routes/api/v1/authenticate'; import { initUsersApi } from './server/routes/api/v1/users'; import { initPublicRolesApi } from './server/routes/api/public/roles'; +import { initPrivilegesApi } from './server/routes/api/public/privileges'; import { initIndicesApi } from './server/routes/api/v1/indices'; import { initLoginView } from './server/routes/views/login'; import { initLogoutView } from './server/routes/views/logout'; @@ -19,10 +20,18 @@ import { checkLicense } from './server/lib/check_license'; import { initAuthenticator } from './server/lib/authentication/authenticator'; import { SecurityAuditLogger } from './server/lib/audit_logger'; import { AuditLogger } from '../../server/lib/audit_logger'; -import { createAuthorizationService, registerPrivilegesWithCluster } from './server/lib/authorization'; +import { + createAuthorizationService, + disableUICapabilitesFactory, + initAPIAuthorization, + initAppAuthorization, + registerPrivilegesWithCluster, + validateFeaturePrivileges +} from './server/lib/authorization'; import { watchStatusAndLicenseToInitialize } from '../../server/lib/watch_status_and_license_to_initialize'; import { SecureSavedObjectsClientWrapper } from './server/lib/saved_objects_client/secure_saved_objects_client_wrapper'; import { deepFreeze } from './server/lib/deep_freeze'; +import { createOptionalPlugin } from './server/lib/optional_plugin'; export const security = (kibana) => new kibana.Plugin({ id: 'security', @@ -93,9 +102,45 @@ export const security = (kibana) => new kibana.Plugin({ sessionTimeout: config.get('xpack.security.sessionTimeout'), enableSpaceAwarePrivileges: config.get('xpack.spaces.enabled'), }; + }, + replaceInjectedVars: async function (originalInjectedVars, request, server) { + // if we have a license which doesn't enable security, or we're a legacy user + // we shouldn't disable any ui capabilities + const { authorization } = server.plugins.security; + if (!authorization.mode.useRbacForRequest(request)) { + return originalInjectedVars; + } + + const disableUICapabilites = disableUICapabilitesFactory(server, request); + // if we're an anonymous route, we disable all ui capabilities + if (request.route.settings.auth === false) { + return { + ...originalInjectedVars, + uiCapabilities: disableUICapabilites.all(originalInjectedVars.uiCapabilities) + }; + } + + return { + ...originalInjectedVars, + uiCapabilities: await disableUICapabilites.usingPrivileges(originalInjectedVars.uiCapabilities) + }; } }, + async postInit(server) { + const plugin = this; + + const xpackMainPlugin = server.plugins.xpack_main; + + watchStatusAndLicenseToInitialize(xpackMainPlugin, plugin, async (license) => { + if (license.allowRbac) { + const { security } = server.plugins; + await validateFeaturePrivileges(security.authorization.actions, xpackMainPlugin.getFeatures()); + await registerPrivilegesWithCluster(server); + } + }); + }, + async init(server) { const plugin = this; @@ -120,19 +165,16 @@ export const security = (kibana) => new kibana.Plugin({ // automatically assigned to all routes that don't contain an auth config. server.auth.default('session'); + const { savedObjects } = server; + + const spaces = createOptionalPlugin(config, 'xpack.spaces', server.plugins, 'spaces'); + // exposes server.plugins.security.authorization - const authorization = createAuthorizationService(server, xpackInfoFeature); + const authorization = createAuthorizationService(server, xpackInfoFeature, xpackMainPlugin, spaces); server.expose('authorization', deepFreeze(authorization)); - watchStatusAndLicenseToInitialize(xpackMainPlugin, plugin, async (license) => { - if (license.allowRbac) { - await registerPrivilegesWithCluster(server); - } - }); - const auditLogger = new SecurityAuditLogger(server.config(), new AuditLogger(server, 'security')); - const { savedObjects } = server; savedObjects.setScopedSavedObjectsClientFactory(({ request, }) => { @@ -151,17 +193,14 @@ export const security = (kibana) => new kibana.Plugin({ savedObjects.addScopedSavedObjectsClientWrapperFactory(Number.MIN_VALUE, ({ client, request }) => { if (authorization.mode.useRbacForRequest(request)) { - const { spaces } = server.plugins; - return new SecureSavedObjectsClientWrapper({ actions: authorization.actions, auditLogger, baseClient: client, - checkPrivilegesWithRequest: authorization.checkPrivilegesWithRequest, + checkPrivilegesDynamicallyWithRequest: authorization.checkPrivilegesDynamicallyWithRequest, errors: savedObjects.SavedObjectsClient.errors, request, savedObjectTypes: savedObjects.types, - spaces, }); } @@ -172,9 +211,12 @@ export const security = (kibana) => new kibana.Plugin({ await initAuthenticator(server); initAuthenticateApi(server); + initAPIAuthorization(server, authorization); + initAppAuthorization(server, xpackMainPlugin, authorization); initUsersApi(server); initPublicRolesApi(server); initIndicesApi(server); + initPrivilegesApi(server); initLoginView(server, xpackMainPlugin); initLogoutView(server); initLoggedOutView(server); diff --git a/x-pack/plugins/security/public/components/_index.scss b/x-pack/plugins/security/public/components/_index.scss new file mode 100644 index 000000000000..707dc73de00f --- /dev/null +++ b/x-pack/plugins/security/public/components/_index.scss @@ -0,0 +1 @@ +@import './management/users/index'; diff --git a/x-pack/plugins/security/public/components/management/users/_index.scss b/x-pack/plugins/security/public/components/management/users/_index.scss new file mode 100644 index 000000000000..c5da74aa3f78 --- /dev/null +++ b/x-pack/plugins/security/public/components/management/users/_index.scss @@ -0,0 +1 @@ +@import './users'; diff --git a/x-pack/plugins/security/public/components/management/users/_users.scss b/x-pack/plugins/security/public/components/management/users/_users.scss new file mode 100644 index 000000000000..d06ec3dd526f --- /dev/null +++ b/x-pack/plugins/security/public/components/management/users/_users.scss @@ -0,0 +1,16 @@ +// HACK -- Fix for background color full-height of browser +.secUsersEditPage, +.secUsersListingPage { + min-height: calc(100vh - 70px); +} + +.secUsersListingPage__content { + flex-grow: 0; +} + +.secUsersEditPage__content { + max-width: $secFormWidth; + margin-left: auto; + margin-right: auto; + flex-grow: 0; +} diff --git a/x-pack/plugins/security/public/components/management/users/edit_user.js b/x-pack/plugins/security/public/components/management/users/edit_user.js index cfb05f83cfa2..d3ce0d4da624 100644 --- a/x-pack/plugins/security/public/components/management/users/edit_user.js +++ b/x-pack/plugins/security/public/components/management/users/edit_user.js @@ -384,8 +384,8 @@ class EditUserUI extends Component { } return ( -
- +
+ diff --git a/x-pack/plugins/security/public/components/management/users/users.js b/x-pack/plugins/security/public/components/management/users/users.js index 4907fbfc1c19..8a526c70355a 100644 --- a/x-pack/plugins/security/public/components/management/users/users.js +++ b/x-pack/plugins/security/public/components/management/users/users.js @@ -91,7 +91,7 @@ class UsersUI extends Component { const { apiClient, intl } = this.props; if (permissionDenied) { return ( -
+
- +
+ diff --git a/x-pack/plugins/security/public/index.scss b/x-pack/plugins/security/public/index.scss index e87a2f5c9528..2d7696bed398 100644 --- a/x-pack/plugins/security/public/index.scss +++ b/x-pack/plugins/security/public/index.scss @@ -1,10 +1,17 @@ @import 'src/legacy/ui/public/styles/styling_constants'; -// Logged out styles -@import './views/logged_out/index'; +// Prefix all styles with "kbn" to avoid conflicts. +// Examples +// secChart +// secChart__legend +// secChart__legend--small +// secChart__legend-isLoading -// Login styles -@import './views/login/index'; +$secFormWidth: 460px; + +// Public components +@import './components/index'; + +// Public views +@import './views/index'; -// Management styles -@import './views/management/index'; diff --git a/x-pack/plugins/security/public/lib/kibana_privilege_calculator/__fixtures__/build_role.ts b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/__fixtures__/build_role.ts new file mode 100644 index 000000000000..cbf3b2a384f7 --- /dev/null +++ b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/__fixtures__/build_role.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { FeaturesPrivileges, Role } from '../../../../common/model'; + +export interface BuildRoleOpts { + spacesPrivileges?: Array<{ + spaces: string[]; + base: string[]; + feature: FeaturesPrivileges; + }>; +} + +export const buildRole = (options: BuildRoleOpts = {}) => { + const role: Role = { + name: 'unit test role', + elasticsearch: { + indices: [], + cluster: [], + run_as: [], + }, + kibana: [], + }; + + if (options.spacesPrivileges) { + role.kibana.push(...options.spacesPrivileges); + } else { + role.kibana.push({ + spaces: [], + base: [], + feature: {}, + }); + } + + return role; +}; diff --git a/x-pack/plugins/security/public/lib/kibana_privilege_calculator/__fixtures__/common_allowed_privileges.ts b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/__fixtures__/common_allowed_privileges.ts new file mode 100644 index 000000000000..3c2582475089 --- /dev/null +++ b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/__fixtures__/common_allowed_privileges.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const unrestrictedBasePrivileges = { + base: { + privileges: ['all', 'read'], + canUnassign: true, + }, +}; +export const unrestrictedFeaturePrivileges = { + feature: { + feature1: { + privileges: ['all', 'read'], + canUnassign: true, + }, + feature2: { + privileges: ['all', 'read'], + canUnassign: true, + }, + feature3: { + privileges: ['all'], + canUnassign: true, + }, + }, +}; + +export const fullyRestrictedBasePrivileges = { + base: { + privileges: ['all'], + canUnassign: false, + }, +}; + +export const fullyRestrictedFeaturePrivileges = { + feature: { + feature1: { + privileges: ['all'], + canUnassign: false, + }, + feature2: { + privileges: ['all'], + canUnassign: false, + }, + feature3: { + privileges: ['all'], + canUnassign: false, + }, + }, +}; diff --git a/x-pack/plugins/security/public/lib/kibana_privilege_calculator/__fixtures__/default_privilege_definition.ts b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/__fixtures__/default_privilege_definition.ts new file mode 100644 index 000000000000..d598a9da67a5 --- /dev/null +++ b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/__fixtures__/default_privilege_definition.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { KibanaPrivileges } from '../../../../common/model'; + +export const defaultPrivilegeDefinition = new KibanaPrivileges({ + global: { + all: ['api:/*', 'ui:/*'], + read: ['ui:/feature1/foo', 'ui:/feature2/foo', 'ui:/feature3/foo/*'], + }, + space: { + all: [ + 'api:/feature1/*', + 'ui:/feature1/*', + 'api:/feature2/*', + 'ui:/feature2/*', + 'ui:/feature3/foo', + 'ui:/feature3/foo/*', + ], + read: ['ui:/feature1/foo', 'ui:/feature2/foo', 'ui:/feature3/foo/bar'], + }, + features: { + feature1: { + all: ['ui:/feature1/foo', 'ui:/feature1/bar'], + read: ['ui:/feature1/foo'], + }, + feature2: { + all: ['ui:/feature2/foo', 'api:/feature2/bar'], + read: ['ui:/feature2/foo'], + }, + feature3: { + all: ['ui:/feature3/foo', 'ui:/feature3/foo/*'], + }, + }, + reserved: {}, +}); diff --git a/x-pack/plugins/security/public/lib/kibana_privilege_calculator/__fixtures__/index.ts b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/__fixtures__/index.ts new file mode 100644 index 000000000000..253dcaed9f19 --- /dev/null +++ b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/__fixtures__/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { defaultPrivilegeDefinition } from './default_privilege_definition'; +export { buildRole, BuildRoleOpts } from './build_role'; +export * from './common_allowed_privileges'; diff --git a/x-pack/plugins/security/public/lib/kibana_privilege_calculator/index.ts b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/index.ts new file mode 100644 index 000000000000..056a4d3022fc --- /dev/null +++ b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { KibanaPrivilegeCalculatorFactory } from './kibana_privileges_calculator_factory'; +export * from './kibana_privilege_calculator_types'; diff --git a/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_allowed_privileges_calculator.test.ts b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_allowed_privileges_calculator.test.ts new file mode 100644 index 000000000000..ac3c74a98776 --- /dev/null +++ b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_allowed_privileges_calculator.test.ts @@ -0,0 +1,296 @@ +/* + * 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 { KibanaPrivileges, Role } from '../../../common/model'; +import { + buildRole, + defaultPrivilegeDefinition, + fullyRestrictedBasePrivileges, + fullyRestrictedFeaturePrivileges, + unrestrictedBasePrivileges, + unrestrictedFeaturePrivileges, +} from './__fixtures__'; +import { KibanaAllowedPrivilegesCalculator } from './kibana_allowed_privileges_calculator'; +import { KibanaPrivilegeCalculatorFactory } from './kibana_privileges_calculator_factory'; + +const buildAllowedPrivilegesCalculator = ( + role: Role, + kibanaPrivilege: KibanaPrivileges = defaultPrivilegeDefinition +) => { + return new KibanaAllowedPrivilegesCalculator(kibanaPrivilege, role); +}; + +const buildEffectivePrivilegesCalculator = ( + role: Role, + kibanaPrivileges: KibanaPrivileges = defaultPrivilegeDefinition +) => { + const factory = new KibanaPrivilegeCalculatorFactory(kibanaPrivileges); + return factory.getInstance(role); +}; + +describe('AllowedPrivileges', () => { + it('allows all privileges when none are currently assigned', () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: [], + feature: {}, + }, + { + spaces: ['foo'], + base: [], + feature: {}, + }, + ], + }); + const effectivePrivileges = buildEffectivePrivilegesCalculator(role); + const allowedPrivilegesCalculator = buildAllowedPrivilegesCalculator(role); + + const result = allowedPrivilegesCalculator.calculateAllowedPrivileges( + effectivePrivileges.calculateEffectivePrivileges(true) + ); + + expect(result).toEqual([ + { + ...unrestrictedBasePrivileges, + ...unrestrictedFeaturePrivileges, + }, + { + ...unrestrictedBasePrivileges, + ...unrestrictedFeaturePrivileges, + }, + ]); + }); + + it('allows all global base privileges, but just "all" for everything else when global is set to "all"', () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: {}, + }, + { + spaces: ['foo'], + base: [], + feature: {}, + }, + ], + }); + const effectivePrivileges = buildEffectivePrivilegesCalculator(role); + const allowedPrivilegesCalculator = buildAllowedPrivilegesCalculator(role); + + const result = allowedPrivilegesCalculator.calculateAllowedPrivileges( + effectivePrivileges.calculateEffectivePrivileges(true) + ); + + expect(result).toEqual([ + { + ...unrestrictedBasePrivileges, + ...fullyRestrictedFeaturePrivileges, + }, + { + ...fullyRestrictedBasePrivileges, + ...fullyRestrictedFeaturePrivileges, + }, + ]); + }); + + it(`allows feature privileges to be set to "all" or "read" when global base is "read"`, () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: ['read'], + feature: {}, + }, + { + spaces: ['foo'], + base: [], + feature: {}, + }, + ], + }); + const effectivePrivileges = buildEffectivePrivilegesCalculator(role); + const allowedPrivilegesCalculator = buildAllowedPrivilegesCalculator(role); + + const result = allowedPrivilegesCalculator.calculateAllowedPrivileges( + effectivePrivileges.calculateEffectivePrivileges(true) + ); + + const expectedFeaturePrivileges = { + feature: { + feature1: { + privileges: ['all', 'read'], + canUnassign: false, + }, + feature2: { + privileges: ['all', 'read'], + canUnassign: false, + }, + feature3: { + privileges: ['all'], + canUnassign: true, // feature 3 has no "read" privilege governed by global "all" + }, + }, + }; + + expect(result).toEqual([ + { + ...unrestrictedBasePrivileges, + ...expectedFeaturePrivileges, + }, + { + base: { + privileges: ['all', 'read'], + canUnassign: false, + }, + ...expectedFeaturePrivileges, + }, + ]); + }); + + it(`allows feature privileges to be set to "all" or "read" when space base is "read"`, () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: [], + feature: {}, + }, + { + spaces: ['foo'], + base: ['read'], + feature: {}, + }, + ], + }); + const effectivePrivileges = buildEffectivePrivilegesCalculator(role); + const allowedPrivilegesCalculator = buildAllowedPrivilegesCalculator(role); + + const result = allowedPrivilegesCalculator.calculateAllowedPrivileges( + effectivePrivileges.calculateEffectivePrivileges(true) + ); + + expect(result).toEqual([ + { + ...unrestrictedBasePrivileges, + ...unrestrictedFeaturePrivileges, + }, + { + base: { + privileges: ['all', 'read'], + canUnassign: true, + }, + feature: { + feature1: { + privileges: ['all', 'read'], + canUnassign: false, + }, + feature2: { + privileges: ['all', 'read'], + canUnassign: false, + }, + feature3: { + privileges: ['all'], + canUnassign: true, // feature 3 has no "read" privilege governed by space "all" + }, + }, + }, + ]); + }); + + it(`allows space base privilege to be set to "all" or "read" when space base is already "all"`, () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['foo'], + base: ['all'], + feature: {}, + }, + ], + }); + const effectivePrivileges = buildEffectivePrivilegesCalculator(role); + const allowedPrivilegesCalculator = buildAllowedPrivilegesCalculator(role); + + const result = allowedPrivilegesCalculator.calculateAllowedPrivileges( + effectivePrivileges.calculateEffectivePrivileges(true) + ); + + expect(result).toEqual([ + { + ...unrestrictedBasePrivileges, + feature: { + feature1: { + privileges: ['all'], + canUnassign: false, + }, + feature2: { + privileges: ['all'], + canUnassign: false, + }, + feature3: { + privileges: ['all'], + canUnassign: false, + }, + }, + }, + ]); + }); + + it(`restricts space feature privileges when global feature privileges are set`, () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: [], + feature: { + feature1: ['all'], + feature2: ['read'], + }, + }, + { + spaces: ['foo'], + base: [], + feature: {}, + }, + ], + }); + const effectivePrivileges = buildEffectivePrivilegesCalculator(role); + const allowedPrivilegesCalculator = buildAllowedPrivilegesCalculator(role); + + const result = allowedPrivilegesCalculator.calculateAllowedPrivileges( + effectivePrivileges.calculateEffectivePrivileges(true) + ); + + expect(result).toEqual([ + { + ...unrestrictedBasePrivileges, + ...unrestrictedFeaturePrivileges, + }, + { + base: { + privileges: ['all', 'read'], + canUnassign: true, + }, + feature: { + feature1: { + privileges: ['all'], + canUnassign: false, + }, + feature2: { + privileges: ['all', 'read'], + canUnassign: false, + }, + feature3: { + privileges: ['all'], + canUnassign: true, // feature 3 has no "read" privilege governed by space "all" + }, + }, + }, + ]); + }); +}); diff --git a/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_allowed_privileges_calculator.ts b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_allowed_privileges_calculator.ts new file mode 100644 index 000000000000..f2e2c4bc1be9 --- /dev/null +++ b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_allowed_privileges_calculator.ts @@ -0,0 +1,153 @@ +/* + * 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 _ from 'lodash'; +import { KibanaPrivileges, Role, RoleKibanaPrivilege } from '../../../common/model'; +import { areActionsFullyCovered, compareActions } from '../../../common/privilege_calculator_utils'; +import { NO_PRIVILEGE_VALUE } from '../../views/management/edit_role/lib/constants'; +import { isGlobalPrivilegeDefinition } from '../privilege_utils'; +import { + AllowedPrivilege, + CalculatedPrivilege, + PRIVILEGE_SOURCE, +} from './kibana_privilege_calculator_types'; + +export class KibanaAllowedPrivilegesCalculator { + // reference to the global privilege definition + private globalPrivilege: RoleKibanaPrivilege; + + // list of privilege actions that comprise the global base privilege + private assignedGlobalBaseActions: string[]; + + constructor(private readonly kibanaPrivileges: KibanaPrivileges, private readonly role: Role) { + this.globalPrivilege = this.locateGlobalPrivilege(role); + this.assignedGlobalBaseActions = this.globalPrivilege.base[0] + ? kibanaPrivileges.getGlobalPrivileges().getActions(this.globalPrivilege.base[0]) + : []; + } + + public calculateAllowedPrivileges( + effectivePrivileges: CalculatedPrivilege[] + ): AllowedPrivilege[] { + const { kibana = [] } = this.role; + return kibana.map((privilegeSpec, index) => + this.calculateAllowedPrivilege(privilegeSpec, effectivePrivileges[index]) + ); + } + + private calculateAllowedPrivilege( + privilegeSpec: RoleKibanaPrivilege, + effectivePrivileges: CalculatedPrivilege + ): AllowedPrivilege { + const result: AllowedPrivilege = { + base: { + privileges: [], + canUnassign: true, + }, + feature: {}, + }; + + if (isGlobalPrivilegeDefinition(privilegeSpec)) { + // nothing can impede global privileges + result.base.canUnassign = true; + result.base.privileges = this.kibanaPrivileges.getGlobalPrivileges().getAllPrivileges(); + } else { + // space base privileges are restricted based on the assigned global privileges + const spacePrivileges = this.kibanaPrivileges.getSpacesPrivileges().getAllPrivileges(); + result.base.canUnassign = this.assignedGlobalBaseActions.length === 0; + result.base.privileges = spacePrivileges.filter(privilege => { + // always allowed to assign the calculated effective privilege + if (privilege === effectivePrivileges.base.actualPrivilege) { + return true; + } + + const privilegeActions = this.getBaseActions(PRIVILEGE_SOURCE.SPACE_BASE, privilege); + return !areActionsFullyCovered(this.assignedGlobalBaseActions, privilegeActions); + }); + } + + const allFeaturePrivileges = this.kibanaPrivileges.getFeaturePrivileges().getAllPrivileges(); + result.feature = Object.entries(allFeaturePrivileges).reduce( + (acc, [featureId, featurePrivileges]) => { + return { + ...acc, + [featureId]: this.getAllowedFeaturePrivileges( + effectivePrivileges, + featureId, + featurePrivileges + ), + }; + }, + {} + ); + + return result; + } + + private getAllowedFeaturePrivileges( + effectivePrivileges: CalculatedPrivilege, + featureId: string, + candidateFeaturePrivileges: string[] + ): { privileges: string[]; canUnassign: boolean } { + const effectiveFeaturePrivilegeExplanation = effectivePrivileges.feature[featureId]; + if (effectiveFeaturePrivilegeExplanation == null) { + throw new Error('To calculate allowed feature privileges, we need the effective privileges'); + } + + const effectiveFeatureActions = this.getFeatureActions( + featureId, + effectiveFeaturePrivilegeExplanation.actualPrivilege + ); + + const privileges = []; + if (effectiveFeaturePrivilegeExplanation.actualPrivilege !== NO_PRIVILEGE_VALUE) { + // Always allowed to assign the calculated effective privilege + privileges.push(effectiveFeaturePrivilegeExplanation.actualPrivilege); + } + + privileges.push( + ...candidateFeaturePrivileges.filter(privilegeId => { + const candidateActions = this.getFeatureActions(featureId, privilegeId); + return compareActions(effectiveFeatureActions, candidateActions) > 0; + }) + ); + + const result = { + privileges: privileges.sort(), + canUnassign: effectiveFeaturePrivilegeExplanation.actualPrivilege === NO_PRIVILEGE_VALUE, + }; + + return result; + } + + private getBaseActions(source: PRIVILEGE_SOURCE, privilegeId: string) { + switch (source) { + case PRIVILEGE_SOURCE.GLOBAL_BASE: + return this.assignedGlobalBaseActions; + case PRIVILEGE_SOURCE.SPACE_BASE: + return this.kibanaPrivileges.getSpacesPrivileges().getActions(privilegeId); + default: + throw new Error( + `Cannot get base actions for unsupported privilege source ${PRIVILEGE_SOURCE[source]}` + ); + } + } + + private getFeatureActions(featureId: string, privilegeId: string): string[] { + return this.kibanaPrivileges.getFeaturePrivileges().getActions(featureId, privilegeId); + } + + private locateGlobalPrivilege(role: Role) { + const spacePrivileges = role.kibana; + return ( + spacePrivileges.find(privileges => isGlobalPrivilegeDefinition(privileges)) || { + spaces: [] as string[], + base: [] as string[], + feature: {}, + } + ); + } +} diff --git a/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_base_privilege_calculator.test.ts b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_base_privilege_calculator.test.ts new file mode 100644 index 000000000000..d5c2727b35b1 --- /dev/null +++ b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_base_privilege_calculator.test.ts @@ -0,0 +1,321 @@ +/* + * 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 { KibanaPrivileges, Role, RoleKibanaPrivilege } from '../../../common/model'; +import { NO_PRIVILEGE_VALUE } from '../../views/management/edit_role/lib/constants'; +import { isGlobalPrivilegeDefinition } from '../privilege_utils'; +import { buildRole, defaultPrivilegeDefinition } from './__fixtures__'; +import { KibanaBasePrivilegeCalculator } from './kibana_base_privilege_calculator'; +import { PRIVILEGE_SOURCE, PrivilegeExplanation } from './kibana_privilege_calculator_types'; + +const buildEffectiveBasePrivilegeCalculator = ( + role: Role, + kibanaPrivileges: KibanaPrivileges = defaultPrivilegeDefinition +) => { + const globalPrivilegeSpec = + role.kibana.find(k => isGlobalPrivilegeDefinition(k)) || + ({ + spaces: ['*'], + base: [], + feature: {}, + } as RoleKibanaPrivilege); + + const globalActions = globalPrivilegeSpec.base[0] + ? kibanaPrivileges.getGlobalPrivileges().getActions(globalPrivilegeSpec.base[0]) + : []; + + return new KibanaBasePrivilegeCalculator(kibanaPrivileges, globalPrivilegeSpec, globalActions); +}; + +describe('getMostPermissiveBasePrivilege', () => { + describe('without ignoring assigned', () => { + it('returns "none" when no privileges are granted', () => { + const role = buildRole(); + const effectiveBasePrivilegesCalculator = buildEffectiveBasePrivilegeCalculator(role); + const result: PrivilegeExplanation = effectiveBasePrivilegesCalculator.getMostPermissiveBasePrivilege( + role.kibana[0], + false + ); + + expect(result).toEqual({ + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: true, + } as PrivilegeExplanation); + }); + + defaultPrivilegeDefinition + .getGlobalPrivileges() + .getAllPrivileges() + .forEach(globalBasePrivilege => { + it(`returns "${globalBasePrivilege}" when assigned directly at the global privilege`, () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: [globalBasePrivilege], + feature: {}, + }, + ], + }); + const effectiveBasePrivilegesCalculator = buildEffectiveBasePrivilegeCalculator(role); + const result: PrivilegeExplanation = effectiveBasePrivilegesCalculator.getMostPermissiveBasePrivilege( + role.kibana[0], + false + ); + + expect(result).toEqual({ + actualPrivilege: globalBasePrivilege, + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: true, + } as PrivilegeExplanation); + }); + }); + + defaultPrivilegeDefinition + .getSpacesPrivileges() + .getAllPrivileges() + .forEach(spaceBasePrivilege => { + it(`returns "${spaceBasePrivilege}" when assigned directly at the space base privilege`, () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['foo'], + base: [spaceBasePrivilege], + feature: {}, + }, + ], + }); + const effectiveBasePrivilegesCalculator = buildEffectiveBasePrivilegeCalculator(role); + const result: PrivilegeExplanation = effectiveBasePrivilegesCalculator.getMostPermissiveBasePrivilege( + role.kibana[0], + false + ); + + expect(result).toEqual({ + actualPrivilege: spaceBasePrivilege, + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE, + isDirectlyAssigned: true, + } as PrivilegeExplanation); + }); + }); + + it('returns the global privilege when no space base is defined', () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: {}, + }, + { + spaces: ['foo'], + base: [], + feature: {}, + }, + ], + }); + const effectiveBasePrivilegesCalculator = buildEffectiveBasePrivilegeCalculator(role); + const result: PrivilegeExplanation = effectiveBasePrivilegesCalculator.getMostPermissiveBasePrivilege( + role.kibana[1], + false + ); + + expect(result).toEqual({ + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + } as PrivilegeExplanation); + }); + + it('returns the global privilege when it supercedes the space privilege', () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: {}, + }, + { + spaces: ['foo'], + base: ['read'], + feature: {}, + }, + ], + }); + const effectiveBasePrivilegesCalculator = buildEffectiveBasePrivilegeCalculator(role); + const result: PrivilegeExplanation = effectiveBasePrivilegesCalculator.getMostPermissiveBasePrivilege( + role.kibana[1], + false + ); + + expect(result).toEqual({ + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + supersededPrivilege: 'read', + supersededPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE, + } as PrivilegeExplanation); + }); + }); + + describe('ignoring assigned', () => { + it('returns "none" when no privileges are granted', () => { + const role = buildRole(); + const effectiveBasePrivilegesCalculator = buildEffectiveBasePrivilegeCalculator(role); + const result: PrivilegeExplanation = effectiveBasePrivilegesCalculator.getMostPermissiveBasePrivilege( + role.kibana[0], + true + ); + + expect(result).toEqual({ + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: true, + } as PrivilegeExplanation); + }); + + defaultPrivilegeDefinition + .getGlobalPrivileges() + .getAllPrivileges() + .forEach(globalBasePrivilege => { + it(`returns "none" when "${globalBasePrivilege}" assigned directly at the global privilege`, () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: [globalBasePrivilege], + feature: {}, + }, + ], + }); + const effectiveBasePrivilegesCalculator = buildEffectiveBasePrivilegeCalculator(role); + const result: PrivilegeExplanation = effectiveBasePrivilegesCalculator.getMostPermissiveBasePrivilege( + role.kibana[0], + true + ); + + expect(result).toEqual({ + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: true, + } as PrivilegeExplanation); + }); + }); + + defaultPrivilegeDefinition + .getSpacesPrivileges() + .getAllPrivileges() + .forEach(spaceBasePrivilege => { + it(`returns "none" when "${spaceBasePrivilege}" when assigned directly at the space base privilege`, () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['foo'], + base: [spaceBasePrivilege], + feature: {}, + }, + ], + }); + const effectiveBasePrivilegesCalculator = buildEffectiveBasePrivilegeCalculator(role); + const result: PrivilegeExplanation = effectiveBasePrivilegesCalculator.getMostPermissiveBasePrivilege( + role.kibana[0], + true + ); + + expect(result).toEqual({ + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE, + isDirectlyAssigned: true, + } as PrivilegeExplanation); + }); + }); + + it('returns the global privilege when no space base is defined', () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: {}, + }, + { + spaces: ['foo'], + base: [], + feature: {}, + }, + ], + }); + const effectiveBasePrivilegesCalculator = buildEffectiveBasePrivilegeCalculator(role); + const result: PrivilegeExplanation = effectiveBasePrivilegesCalculator.getMostPermissiveBasePrivilege( + role.kibana[1], + true + ); + + expect(result).toEqual({ + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + } as PrivilegeExplanation); + }); + + it('returns the global privilege when it supercedes the space privilege, without indicating override', () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: {}, + }, + { + spaces: ['foo'], + base: ['read'], + feature: {}, + }, + ], + }); + const effectiveBasePrivilegesCalculator = buildEffectiveBasePrivilegeCalculator(role); + const result: PrivilegeExplanation = effectiveBasePrivilegesCalculator.getMostPermissiveBasePrivilege( + role.kibana[1], + true + ); + + expect(result).toEqual({ + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + } as PrivilegeExplanation); + }); + + it('returns the global privilege even though it would ordinarly be overriden by space base privilege', () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: ['read'], + feature: {}, + }, + { + spaces: ['foo'], + base: ['all'], + feature: {}, + }, + ], + }); + const effectiveBasePrivilegesCalculator = buildEffectiveBasePrivilegeCalculator(role); + const result: PrivilegeExplanation = effectiveBasePrivilegesCalculator.getMostPermissiveBasePrivilege( + role.kibana[1], + true + ); + + expect(result).toEqual({ + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + } as PrivilegeExplanation); + }); + }); +}); diff --git a/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_base_privilege_calculator.ts b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_base_privilege_calculator.ts new file mode 100644 index 000000000000..37ed5b6c02e9 --- /dev/null +++ b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_base_privilege_calculator.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import _ from 'lodash'; +import { KibanaPrivileges, RoleKibanaPrivilege } from '../../../common/model'; +import { compareActions } from '../../../common/privilege_calculator_utils'; +import { NO_PRIVILEGE_VALUE } from '../../views/management/edit_role/lib/constants'; +import { isGlobalPrivilegeDefinition } from '../privilege_utils'; +import { PRIVILEGE_SOURCE, PrivilegeExplanation } from './kibana_privilege_calculator_types'; + +export class KibanaBasePrivilegeCalculator { + constructor( + private readonly kibanaPrivileges: KibanaPrivileges, + private readonly globalPrivilege: RoleKibanaPrivilege, + private readonly assignedGlobalBaseActions: string[] + ) {} + + public getMostPermissiveBasePrivilege( + privilegeSpec: RoleKibanaPrivilege, + ignoreAssigned: boolean + ): PrivilegeExplanation { + const assignedPrivilege = privilegeSpec.base[0] || NO_PRIVILEGE_VALUE; + + // If this is the global privilege definition, then there is nothing to supercede it. + if (isGlobalPrivilegeDefinition(privilegeSpec)) { + if (assignedPrivilege === NO_PRIVILEGE_VALUE || ignoreAssigned) { + return { + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: true, + }; + } + return { + actualPrivilege: assignedPrivilege, + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: true, + }; + } + + // Otherwise, check to see if the global privilege supercedes this one. + const baseActions = [ + ...this.kibanaPrivileges.getSpacesPrivileges().getActions(assignedPrivilege), + ]; + + const globalSupercedes = + this.hasAssignedGlobalBasePrivilege() && + (compareActions(this.assignedGlobalBaseActions, baseActions) < 0 || ignoreAssigned); + + if (globalSupercedes) { + const wasDirectlyAssigned = !ignoreAssigned && baseActions.length > 0; + + return { + actualPrivilege: this.globalPrivilege.base[0], + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + ...this.buildSupercededFields( + wasDirectlyAssigned, + assignedPrivilege, + PRIVILEGE_SOURCE.SPACE_BASE + ), + }; + } + + if (!ignoreAssigned) { + return { + actualPrivilege: assignedPrivilege, + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE, + isDirectlyAssigned: true, + }; + } + + return { + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE, + isDirectlyAssigned: true, + }; + } + + private hasAssignedGlobalBasePrivilege() { + return this.assignedGlobalBaseActions.length > 0; + } + + private buildSupercededFields( + isSuperceding: boolean, + supersededPrivilege?: string, + supersededPrivilegeSource?: PRIVILEGE_SOURCE + ) { + if (!isSuperceding) { + return {}; + } + return { + supersededPrivilege, + supersededPrivilegeSource, + }; + } +} diff --git a/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_feature_privilege_calculator.test.ts b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_feature_privilege_calculator.test.ts new file mode 100644 index 000000000000..d21255dc1c51 --- /dev/null +++ b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_feature_privilege_calculator.test.ts @@ -0,0 +1,868 @@ +/* + * 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 { KibanaPrivileges, Role, RoleKibanaPrivilege } from '../../../common/model'; +import { NO_PRIVILEGE_VALUE } from '../../views/management/edit_role/lib/constants'; +import { isGlobalPrivilegeDefinition } from '../privilege_utils'; +import { buildRole, BuildRoleOpts, defaultPrivilegeDefinition } from './__fixtures__'; +import { KibanaBasePrivilegeCalculator } from './kibana_base_privilege_calculator'; +import { KibanaFeaturePrivilegeCalculator } from './kibana_feature_privilege_calculator'; +import { PRIVILEGE_SOURCE } from './kibana_privilege_calculator_types'; + +const buildEffectiveBasePrivilegeCalculator = ( + role: Role, + kibanaPrivileges: KibanaPrivileges = defaultPrivilegeDefinition +) => { + const globalPrivilegeSpec = + role.kibana.find(k => isGlobalPrivilegeDefinition(k)) || + ({ + spaces: ['*'], + base: [], + feature: {}, + } as RoleKibanaPrivilege); + + const globalActions = globalPrivilegeSpec.base[0] + ? kibanaPrivileges.getGlobalPrivileges().getActions(globalPrivilegeSpec.base[0]) + : []; + + return new KibanaBasePrivilegeCalculator(kibanaPrivileges, globalPrivilegeSpec, globalActions); +}; + +const buildEffectiveFeaturePrivilegeCalculator = ( + role: Role, + kibanaPrivileges: KibanaPrivileges = defaultPrivilegeDefinition +) => { + const globalPrivilegeSpec = + role.kibana.find(k => isGlobalPrivilegeDefinition(k)) || + ({ + spaces: ['*'], + base: [], + feature: {}, + } as RoleKibanaPrivilege); + + const globalActions = globalPrivilegeSpec.base[0] + ? kibanaPrivileges.getGlobalPrivileges().getActions(globalPrivilegeSpec.base[0]) + : []; + + const rankedFeaturePrivileges = kibanaPrivileges.getFeaturePrivileges().getAllPrivileges(); + + return new KibanaFeaturePrivilegeCalculator( + kibanaPrivileges, + globalPrivilegeSpec, + globalActions, + rankedFeaturePrivileges + ); +}; + +interface TestOpts { + only?: boolean; + role?: BuildRoleOpts; + privilegeIndex?: number; + ignoreAssigned?: boolean; + result: Record; +} + +function runTest( + description: string, + { + role: roleOpts = {}, + result = {}, + privilegeIndex = 0, + ignoreAssigned = false, + only = false, + }: TestOpts +) { + const fn = only ? it.only : it; + fn(description, () => { + const role = buildRole(roleOpts); + const basePrivilegeCalculator = buildEffectiveBasePrivilegeCalculator(role); + const featurePrivilegeCalculator = buildEffectiveFeaturePrivilegeCalculator(role); + + const baseExplanation = basePrivilegeCalculator.getMostPermissiveBasePrivilege( + role.kibana[privilegeIndex], + // If calculations wish to ignoreAssigned, then we still need to know what the real effective base privilege is + // without ignoring assigned, in order to calculate the correct feature privileges. + false + ); + + const actualResult = featurePrivilegeCalculator.getMostPermissiveFeaturePrivilege( + role.kibana[privilegeIndex], + baseExplanation, + 'feature1', + ignoreAssigned + ); + + expect(actualResult).toEqual(result); + }); +} + +describe('getMostPermissiveFeaturePrivilege', () => { + describe('for global feature privileges, without ignoring assigned', () => { + runTest('returns "none" when no privileges are granted', { + result: { + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + isDirectlyAssigned: true, + }, + }); + + runTest('returns "read" when assigned directly to the feature', { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: [], + feature: { + feature1: ['read'], + }, + }, + ], + }, + result: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + isDirectlyAssigned: true, + }, + }); + + runTest('returns "read" when assigned as the global base privilege', { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['read'], + feature: {}, + }, + ], + }, + result: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + }); + + runTest( + 'returns "all" when assigned as the global base privilege, which overrides assigned feature privilege', + { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: { + feature1: ['read'], + }, + }, + ], + }, + result: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + supersededPrivilege: 'read', + supersededPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + }, + } + ); + + runTest( + 'returns "all" when assigned as the feature privilege, which does not override assigned global base privilege', + { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['read'], + feature: { + feature1: ['all'], + }, + }, + ], + }, + result: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + isDirectlyAssigned: true, + }, + } + ); + }); + + describe('for global feature privileges, ignoring assigned', () => { + runTest('returns "none" when no privileges are granted', { + ignoreAssigned: true, + result: { + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + isDirectlyAssigned: true, + }, + }); + + runTest('returns "none" when "read" is assigned directly to the feature', { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: [], + feature: { + feature1: ['read'], + }, + }, + ], + }, + ignoreAssigned: true, + result: { + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + isDirectlyAssigned: true, + }, + }); + + runTest('returns "read" when assigned as the global base privilege', { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['read'], + feature: {}, + }, + ], + }, + ignoreAssigned: true, + result: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + }); + + runTest( + 'returns "all" when assigned as the global base privilege, which overrides assigned feature privilege', + { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: { + feature1: ['read'], + }, + }, + ], + }, + ignoreAssigned: true, + result: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + } + ); + + runTest( + 'returns "read" when "all" assigned as the feature privilege, which does not override assigned global base privilege', + { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['read'], + feature: { + feature1: ['all'], + }, + }, + ], + }, + ignoreAssigned: true, + result: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + } + ); + }); + + describe('for space feature privileges, without ignoring assigned', () => { + runTest('returns "none" when no privileges are granted', { + role: { + spacesPrivileges: [ + { + spaces: ['marketing'], + base: [], + feature: {}, + }, + ], + }, + result: { + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + isDirectlyAssigned: true, + }, + }); + + runTest('returns "read" when assigned directly to the feature', { + role: { + spacesPrivileges: [ + { + spaces: ['marketing'], + base: [], + feature: { + feature1: ['read'], + }, + }, + ], + }, + result: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + isDirectlyAssigned: true, + }, + }); + + runTest('returns "read" when assigned as the global base privilege', { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['read'], + feature: {}, + }, + { + spaces: ['marketing'], + base: [], + feature: {}, + }, + ], + }, + privilegeIndex: 1, + result: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + }); + + runTest( + 'returns "all" when assigned as the global base privilege, which overrides assigned global feature privilege', + { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: { + feature1: ['read'], + }, + }, + { + spaces: ['marketing'], + base: [], + feature: {}, + }, + ], + }, + privilegeIndex: 1, + result: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + } + ); + + runTest( + 'returns "all" when assigned as the global base privilege, which overrides assigned space feature privilege', + { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: {}, + }, + { + spaces: ['marketing'], + base: [], + feature: { + feature1: ['read'], + }, + }, + ], + }, + privilegeIndex: 1, + result: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + supersededPrivilege: 'read', + supersededPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + }, + } + ); + + runTest( + 'returns "all" when assigned as the global feature privilege, which does not override assigned global base privilege', + { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['read'], + feature: { + feature1: ['all'], + }, + }, + { + spaces: ['marketing'], + base: [], + feature: {}, + }, + ], + }, + privilegeIndex: 1, + result: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + isDirectlyAssigned: false, + }, + } + ); + + runTest( + 'returns "all" when assigned as the space feature privilege, which does not override assigned global base privilege', + { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['read'], + feature: {}, + }, + { + spaces: ['marketing'], + base: [], + feature: { + feature1: ['all'], + }, + }, + ], + }, + privilegeIndex: 1, + result: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + isDirectlyAssigned: true, + }, + } + ); + + runTest( + 'returns "all" when assigned as the space base privilege, which does not override assigned global base privilege', + { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['read'], + feature: {}, + }, + { + spaces: ['marketing'], + base: ['all'], + feature: {}, + }, + ], + }, + privilegeIndex: 1, + result: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE, + isDirectlyAssigned: false, + }, + } + ); + + runTest( + 'returns "all" when assigned as the global base privilege, which overrides assigned space feature privilege', + { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: {}, + }, + { + spaces: ['marketing'], + base: [], + feature: { + feature1: ['read'], + }, + }, + ], + }, + privilegeIndex: 1, + result: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + supersededPrivilege: 'read', + supersededPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + }, + } + ); + + runTest('returns "all" when assigned everywhere, without indicating override', { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: { + feature1: ['all'], + }, + }, + { + spaces: ['marketing'], + base: ['all'], + feature: { + feature1: ['all'], + }, + }, + ], + }, + privilegeIndex: 1, + result: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + isDirectlyAssigned: true, + }, + }); + + runTest('returns "all" when assigned at global feature, overriding space feature', { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: [], + feature: { + feature1: ['all'], + }, + }, + { + spaces: ['marketing'], + base: [], + feature: { + feature1: ['read'], + }, + }, + ], + }, + privilegeIndex: 1, + result: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + isDirectlyAssigned: false, + supersededPrivilege: 'read', + supersededPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + }, + }); + }); + + describe('for space feature privileges, ignoring assigned', () => { + runTest('returns "none" when no privileges are granted', { + role: { + spacesPrivileges: [ + { + spaces: ['marketing'], + base: [], + feature: {}, + }, + ], + }, + ignoreAssigned: true, + result: { + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + isDirectlyAssigned: true, + }, + }); + + runTest('returns "none" when "read" assigned directly to the feature', { + role: { + spacesPrivileges: [ + { + spaces: ['marketing'], + base: [], + feature: { + feature1: ['read'], + }, + }, + ], + }, + ignoreAssigned: true, + result: { + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + isDirectlyAssigned: true, + }, + }); + + runTest('returns "read" when assigned as the global base privilege', { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['read'], + feature: {}, + }, + { + spaces: ['marketing'], + base: [], + feature: {}, + }, + ], + }, + privilegeIndex: 1, + ignoreAssigned: true, + result: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + }); + + runTest( + 'returns "all" when assigned as the global base privilege, which overrides assigned global feature privilege', + { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: { + feature1: ['read'], + }, + }, + { + spaces: ['marketing'], + base: [], + feature: {}, + }, + ], + }, + privilegeIndex: 1, + ignoreAssigned: true, + result: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + } + ); + + runTest( + 'returns "all" when assigned as the global base privilege, which normally overrides assigned space feature privilege', + { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: {}, + }, + { + spaces: ['marketing'], + base: [], + feature: { + feature1: ['read'], + }, + }, + ], + }, + privilegeIndex: 1, + ignoreAssigned: true, + result: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + } + ); + + runTest( + 'returns "all" when assigned as the global feature privilege, which does not override assigned global base privilege', + { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['read'], + feature: { + feature1: ['all'], + }, + }, + { + spaces: ['marketing'], + base: [], + feature: {}, + }, + ], + }, + privilegeIndex: 1, + ignoreAssigned: true, + result: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + isDirectlyAssigned: false, + }, + } + ); + + runTest( + 'returns "read" when "all" assigned as the space feature privilege, which normally overrides assigned global base privilege', + { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['read'], + feature: {}, + }, + { + spaces: ['marketing'], + base: [], + feature: { + feature1: ['all'], + }, + }, + ], + }, + privilegeIndex: 1, + ignoreAssigned: true, + result: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + } + ); + + runTest( + 'returns "all" when assigned as the space base privilege, which does not override assigned global base privilege', + { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['read'], + feature: {}, + }, + { + spaces: ['marketing'], + base: ['all'], + feature: {}, + }, + ], + }, + privilegeIndex: 1, + ignoreAssigned: true, + result: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE, + isDirectlyAssigned: false, + }, + } + ); + + runTest( + 'returns "all" when assigned as the global base privilege, which normally overrides assigned space feature privilege', + { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: {}, + }, + { + spaces: ['marketing'], + base: [], + feature: { + feature1: ['read'], + }, + }, + ], + }, + privilegeIndex: 1, + ignoreAssigned: true, + result: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + } + ); + + runTest('returns "all" when assigned everywhere, without indicating override', { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: { + feature1: ['all'], + }, + }, + { + spaces: ['marketing'], + base: ['all'], + feature: { + feature1: ['all'], + }, + }, + ], + }, + privilegeIndex: 1, + ignoreAssigned: true, + result: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE, + isDirectlyAssigned: false, + }, + }); + + runTest('returns "all" when assigned at global feature, normally overriding space feature', { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: [], + feature: { + feature1: ['all'], + }, + }, + { + spaces: ['marketing'], + base: [], + feature: { + feature1: ['read'], + }, + }, + ], + }, + privilegeIndex: 1, + ignoreAssigned: true, + result: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + isDirectlyAssigned: false, + }, + }); + }); +}); diff --git a/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_feature_privilege_calculator.ts b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_feature_privilege_calculator.ts new file mode 100644 index 000000000000..1cdc70878ecd --- /dev/null +++ b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_feature_privilege_calculator.ts @@ -0,0 +1,198 @@ +/* + * 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 _ from 'lodash'; +import { FeaturesPrivileges, KibanaPrivileges, RoleKibanaPrivilege } from '../../../common/model'; +import { areActionsFullyCovered } from '../../../common/privilege_calculator_utils'; +import { NO_PRIVILEGE_VALUE } from '../../views/management/edit_role/lib/constants'; +import { isGlobalPrivilegeDefinition } from '../privilege_utils'; +import { + PRIVILEGE_SOURCE, + PrivilegeExplanation, + PrivilegeScenario, +} from './kibana_privilege_calculator_types'; + +export class KibanaFeaturePrivilegeCalculator { + constructor( + private readonly kibanaPrivileges: KibanaPrivileges, + private readonly globalPrivilege: RoleKibanaPrivilege, + private readonly assignedGlobalBaseActions: string[], + private readonly rankedFeaturePrivileges: FeaturesPrivileges + ) {} + + public getMostPermissiveFeaturePrivilege( + privilegeSpec: RoleKibanaPrivilege, + basePrivilegeExplanation: PrivilegeExplanation, + featureId: string, + ignoreAssigned: boolean + ): PrivilegeExplanation { + const scenarios = this.buildFeaturePrivilegeScenarios( + privilegeSpec, + basePrivilegeExplanation, + featureId, + ignoreAssigned + ); + + const featurePrivileges = this.rankedFeaturePrivileges[featureId] || []; + + // inspect feature privileges in ranked order (most permissive -> least permissive) + for (const featurePrivilege of featurePrivileges) { + const actions = this.kibanaPrivileges + .getFeaturePrivileges() + .getActions(featureId, featurePrivilege); + + // check if any of the scenarios satisfy the privilege - first one wins. + for (const scenario of scenarios) { + if (areActionsFullyCovered(scenario.actions, actions)) { + return { + actualPrivilege: featurePrivilege, + actualPrivilegeSource: scenario.actualPrivilegeSource, + isDirectlyAssigned: scenario.isDirectlyAssigned, + ...this.buildSupercededFields( + !scenario.isDirectlyAssigned, + scenario.supersededPrivilege, + scenario.supersededPrivilegeSource + ), + }; + } + } + } + + const isGlobal = isGlobalPrivilegeDefinition(privilegeSpec); + return { + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: isGlobal + ? PRIVILEGE_SOURCE.GLOBAL_FEATURE + : PRIVILEGE_SOURCE.SPACE_FEATURE, + isDirectlyAssigned: true, + }; + } + + private buildFeaturePrivilegeScenarios( + privilegeSpec: RoleKibanaPrivilege, + basePrivilegeExplanation: PrivilegeExplanation, + featureId: string, + ignoreAssigned: boolean + ): PrivilegeScenario[] { + const scenarios: PrivilegeScenario[] = []; + + const isGlobalPrivilege = isGlobalPrivilegeDefinition(privilegeSpec); + + const assignedGlobalFeaturePrivilege = this.getAssignedFeaturePrivilege( + this.globalPrivilege, + featureId + ); + + const assignedFeaturePrivilege = this.getAssignedFeaturePrivilege(privilegeSpec, featureId); + const hasAssignedFeaturePrivilege = + !ignoreAssigned && assignedFeaturePrivilege !== NO_PRIVILEGE_VALUE; + + scenarios.push({ + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + actions: [...this.assignedGlobalBaseActions], + ...this.buildSupercededFields( + hasAssignedFeaturePrivilege, + assignedFeaturePrivilege, + isGlobalPrivilege ? PRIVILEGE_SOURCE.GLOBAL_FEATURE : PRIVILEGE_SOURCE.SPACE_FEATURE + ), + }); + + if (!isGlobalPrivilege || !ignoreAssigned) { + scenarios.push({ + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + actions: this.getFeatureActions(featureId, assignedGlobalFeaturePrivilege), + isDirectlyAssigned: isGlobalPrivilege && hasAssignedFeaturePrivilege, + ...this.buildSupercededFields( + hasAssignedFeaturePrivilege && !isGlobalPrivilege, + assignedFeaturePrivilege, + PRIVILEGE_SOURCE.SPACE_FEATURE + ), + }); + } + + if (isGlobalPrivilege) { + return this.rankScenarios(scenarios); + } + + // Otherwise, this is a space feature privilege + + const includeSpaceBaseScenario = + basePrivilegeExplanation.actualPrivilegeSource === PRIVILEGE_SOURCE.SPACE_BASE || + basePrivilegeExplanation.supersededPrivilegeSource === PRIVILEGE_SOURCE.SPACE_BASE; + + const spaceBasePrivilege = + basePrivilegeExplanation.supersededPrivilege || basePrivilegeExplanation.actualPrivilege; + + if (includeSpaceBaseScenario) { + scenarios.push({ + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE, + isDirectlyAssigned: false, + actions: this.getBaseActions(PRIVILEGE_SOURCE.SPACE_BASE, spaceBasePrivilege), + ...this.buildSupercededFields( + hasAssignedFeaturePrivilege, + assignedFeaturePrivilege, + PRIVILEGE_SOURCE.SPACE_FEATURE + ), + }); + } + + if (!ignoreAssigned) { + scenarios.push({ + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + isDirectlyAssigned: true, + actions: this.getFeatureActions( + featureId, + this.getAssignedFeaturePrivilege(privilegeSpec, featureId) + ), + }); + } + + return this.rankScenarios(scenarios); + } + + private rankScenarios(scenarios: PrivilegeScenario[]): PrivilegeScenario[] { + return scenarios.sort( + (scenario1, scenario2) => scenario1.actualPrivilegeSource - scenario2.actualPrivilegeSource + ); + } + + private getBaseActions(source: PRIVILEGE_SOURCE, privilegeId: string) { + switch (source) { + case PRIVILEGE_SOURCE.GLOBAL_BASE: + return this.assignedGlobalBaseActions; + case PRIVILEGE_SOURCE.SPACE_BASE: + return this.kibanaPrivileges.getSpacesPrivileges().getActions(privilegeId); + default: + throw new Error( + `Cannot get base actions for unsupported privilege source ${PRIVILEGE_SOURCE[source]}` + ); + } + } + + private getFeatureActions(featureId: string, privilegeId: string) { + return this.kibanaPrivileges.getFeaturePrivileges().getActions(featureId, privilegeId); + } + + private getAssignedFeaturePrivilege(privilegeSpec: RoleKibanaPrivilege, featureId: string) { + const featureEntry = privilegeSpec.feature[featureId] || []; + return featureEntry[0] || NO_PRIVILEGE_VALUE; + } + + private buildSupercededFields( + isSuperceding: boolean, + supersededPrivilege?: string, + supersededPrivilegeSource?: PRIVILEGE_SOURCE + ) { + if (!isSuperceding) { + return {}; + } + return { + supersededPrivilege, + supersededPrivilegeSource, + }; + } +} diff --git a/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_privilege_calculator.test.ts b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_privilege_calculator.test.ts new file mode 100644 index 000000000000..e6b4f67e6acf --- /dev/null +++ b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_privilege_calculator.test.ts @@ -0,0 +1,791 @@ +/* + * 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 { KibanaPrivileges, Role } from '../../../common/model'; +import { NO_PRIVILEGE_VALUE } from '../../views/management/edit_role/lib/constants'; +import { + buildRole, + defaultPrivilegeDefinition, + fullyRestrictedBasePrivileges, + fullyRestrictedFeaturePrivileges, + unrestrictedBasePrivileges, + unrestrictedFeaturePrivileges, +} from './__fixtures__'; +import { + AllowedPrivilege, + PRIVILEGE_SOURCE, + PrivilegeExplanation, +} from './kibana_privilege_calculator_types'; +import { KibanaPrivilegeCalculatorFactory } from './kibana_privileges_calculator_factory'; + +const buildEffectivePrivileges = ( + role: Role, + kibanaPrivileges: KibanaPrivileges = defaultPrivilegeDefinition +) => { + const factory = new KibanaPrivilegeCalculatorFactory(kibanaPrivileges); + return factory.getInstance(role); +}; + +const buildExpectedFeaturePrivileges = ( + expectedFeaturePrivileges: PrivilegeExplanation | { [featureId: string]: PrivilegeExplanation } +) => { + if (expectedFeaturePrivileges.hasOwnProperty('actualPrivilege')) { + return { + feature: { + feature1: expectedFeaturePrivileges, + feature2: expectedFeaturePrivileges, + feature3: expectedFeaturePrivileges, + }, + }; + } + + return { + feature: { + ...expectedFeaturePrivileges, + }, + }; +}; + +describe('calculateEffectivePrivileges', () => { + it(`returns an empty array for an empty role`, () => { + const role = buildRole(); + role.kibana = []; + + const effectivePrivileges = buildEffectivePrivileges(role); + const calculatedPrivileges = effectivePrivileges.calculateEffectivePrivileges(); + expect(calculatedPrivileges).toHaveLength(0); + }); + + it(`calculates "none" for all privileges when nothing is assigned`, () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['foo', 'bar'], + base: [], + feature: {}, + }, + ], + }); + const effectivePrivileges = buildEffectivePrivileges(role); + const calculatedPrivileges = effectivePrivileges.calculateEffectivePrivileges(); + expect(calculatedPrivileges).toEqual([ + { + base: { + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE, + isDirectlyAssigned: true, + }, + ...buildExpectedFeaturePrivileges({ + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + isDirectlyAssigned: true, + }), + }, + ]); + }); + + describe(`with global base privilege of "all"`, () => { + it(`calculates global feature privileges === all`, () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: {}, + }, + ], + }); + const effectivePrivileges = buildEffectivePrivileges(role); + const calculatedPrivileges = effectivePrivileges.calculateEffectivePrivileges(); + + expect(calculatedPrivileges).toEqual([ + { + base: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: true, + }, + ...buildExpectedFeaturePrivileges({ + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }), + }, + ]); + }); + + it(`calculates space base and feature privileges === all`, () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: {}, + }, + { + spaces: ['foo'], + base: [], + feature: {}, + }, + ], + }); + const effectivePrivileges = buildEffectivePrivileges(role); + const calculatedPrivileges = effectivePrivileges.calculateEffectivePrivileges(); + + const calculatedSpacePrivileges = calculatedPrivileges[1]; + + expect(calculatedSpacePrivileges).toEqual({ + base: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + ...buildExpectedFeaturePrivileges({ + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }), + }); + }); + + describe(`and with feature privileges assigned`, () => { + it('returns the base privileges when they are more permissive', () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: { + feature1: ['read'], + feature2: ['read'], + feature3: ['read'], + }, + }, + { + spaces: ['foo'], + base: [], + feature: { + feature1: ['read'], + feature2: ['read'], + feature3: ['read'], + }, + }, + ], + }); + const effectivePrivileges = buildEffectivePrivileges(role); + const calculatedPrivileges = effectivePrivileges.calculateEffectivePrivileges(); + + expect(calculatedPrivileges).toEqual([ + { + base: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: true, + }, + ...buildExpectedFeaturePrivileges({ + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + supersededPrivilege: 'read', + supersededPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + }), + }, + { + base: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + ...buildExpectedFeaturePrivileges({ + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + supersededPrivilege: 'read', + supersededPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + }), + }, + ]); + }); + }); + }); + + describe(`with global base privilege of "read"`, () => { + it(`it calculates space base and feature privileges when none are provided`, () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: ['read'], + feature: {}, + }, + { + spaces: ['foo'], + base: [], + feature: {}, + }, + ], + }); + const effectivePrivileges = buildEffectivePrivileges(role); + const calculatedPrivileges = effectivePrivileges.calculateEffectivePrivileges(); + + expect(calculatedPrivileges).toEqual([ + { + base: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: true, + }, + ...buildExpectedFeaturePrivileges({ + feature1: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + feature2: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + feature3: { + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + isDirectlyAssigned: true, + }, + }), + }, + { + base: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + ...buildExpectedFeaturePrivileges({ + feature1: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + feature2: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + feature3: { + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + isDirectlyAssigned: true, + }, + }), + }, + ]); + }); + + describe('and with feature privileges assigned', () => { + it('returns the feature privileges when they are more permissive', () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: ['read'], + feature: { + feature1: ['all'], + feature2: ['all'], + feature3: ['all'], + }, + }, + { + spaces: ['foo'], + base: [], + feature: { + feature1: ['all'], + feature2: ['all'], + feature3: ['all'], + }, + }, + ], + }); + const effectivePrivileges = buildEffectivePrivileges(role); + const calculatedPrivileges = effectivePrivileges.calculateEffectivePrivileges(); + + expect(calculatedPrivileges).toEqual([ + { + base: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: true, + }, + ...buildExpectedFeaturePrivileges({ + feature1: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + isDirectlyAssigned: true, + }, + feature2: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + isDirectlyAssigned: true, + }, + feature3: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + isDirectlyAssigned: true, + }, + }), + }, + { + base: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + ...buildExpectedFeaturePrivileges({ + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + isDirectlyAssigned: true, + }), + }, + ]); + }); + }); + }); + + describe('with both global and space base privileges assigned', () => { + it(`does not override space base of "all" when global base is "read"`, () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: ['read'], + feature: {}, + }, + { + spaces: ['foo'], + base: ['all'], + feature: {}, + }, + ], + }); + const effectivePrivileges = buildEffectivePrivileges(role); + const calculatedPrivileges = effectivePrivileges.calculateEffectivePrivileges(); + + expect(calculatedPrivileges).toEqual([ + { + base: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: true, + }, + ...buildExpectedFeaturePrivileges({ + feature1: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + feature2: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + feature3: { + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + isDirectlyAssigned: true, + }, + }), + }, + { + base: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE, + isDirectlyAssigned: true, + }, + ...buildExpectedFeaturePrivileges({ + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE, + isDirectlyAssigned: false, + }), + }, + ]); + }); + + it(`calcualtes "all" for space base and space features when superceded by global "all"`, () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: {}, + }, + { + spaces: ['foo'], + base: ['read'], + feature: {}, + }, + ], + }); + const effectivePrivileges = buildEffectivePrivileges(role); + const calculatedPrivileges = effectivePrivileges.calculateEffectivePrivileges(); + + expect(calculatedPrivileges).toEqual([ + { + base: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: true, + }, + ...buildExpectedFeaturePrivileges({ + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }), + }, + { + base: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + supersededPrivilege: 'read', + supersededPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE, + }, + ...buildExpectedFeaturePrivileges({ + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }), + }, + ]); + }); + + it(`does not override feature privileges when they are more permissive`, () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: ['read'], + feature: {}, + }, + { + spaces: ['foo'], + base: ['read'], + feature: { + feature1: ['all'], + feature2: ['all'], + feature3: ['all'], + }, + }, + ], + }); + const effectivePrivileges = buildEffectivePrivileges(role); + const calculatedPrivileges = effectivePrivileges.calculateEffectivePrivileges(); + + expect(calculatedPrivileges).toEqual([ + { + base: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: true, + }, + ...buildExpectedFeaturePrivileges({ + feature1: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + feature2: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + feature3: { + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + isDirectlyAssigned: true, + }, + }), + }, + { + base: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + supersededPrivilege: 'read', + supersededPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE, + }, + ...buildExpectedFeaturePrivileges({ + feature1: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + isDirectlyAssigned: true, + }, + feature2: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + isDirectlyAssigned: true, + }, + feature3: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + isDirectlyAssigned: true, + }, + }), + }, + ]); + }); + }); +}); + +describe('calculateAllowedPrivileges', () => { + it('allows all privileges when none are currently assigned', () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: [], + feature: {}, + }, + { + spaces: ['foo'], + base: [], + feature: {}, + }, + ], + }); + + const privilegeCalculator = buildEffectivePrivileges(role); + + const result: AllowedPrivilege[] = privilegeCalculator.calculateAllowedPrivileges(); + + expect(result).toEqual([ + { + ...unrestrictedBasePrivileges, + ...unrestrictedFeaturePrivileges, + }, + { + ...unrestrictedBasePrivileges, + ...unrestrictedFeaturePrivileges, + }, + ]); + }); + + it('allows all global base privileges, but just "all" for everything else when global is set to "all"', () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: {}, + }, + { + spaces: ['foo'], + base: [], + feature: {}, + }, + ], + }); + + const privilegeCalculator = buildEffectivePrivileges(role); + + const result: AllowedPrivilege[] = privilegeCalculator.calculateAllowedPrivileges(); + + expect(result).toEqual([ + { + ...unrestrictedBasePrivileges, + ...fullyRestrictedFeaturePrivileges, + }, + { + ...fullyRestrictedBasePrivileges, + ...fullyRestrictedFeaturePrivileges, + }, + ]); + }); + + it(`allows feature privileges to be set to "all" or "read" when global base is "read"`, () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: ['read'], + feature: {}, + }, + { + spaces: ['foo'], + base: [], + feature: {}, + }, + ], + }); + + const privilegeCalculator = buildEffectivePrivileges(role); + + const result: AllowedPrivilege[] = privilegeCalculator.calculateAllowedPrivileges(); + + const expectedFeaturePrivileges = { + feature: { + feature1: { + privileges: ['all', 'read'], + canUnassign: false, + }, + feature2: { + privileges: ['all', 'read'], + canUnassign: false, + }, + feature3: { + privileges: ['all'], + canUnassign: true, // feature 3 has no "read" privilege governed by global "all" + }, + }, + }; + + expect(result).toEqual([ + { + ...unrestrictedBasePrivileges, + ...expectedFeaturePrivileges, + }, + { + base: { + privileges: ['all', 'read'], + canUnassign: false, + }, + ...expectedFeaturePrivileges, + }, + ]); + }); + + it(`allows feature privileges to be set to "all" or "read" when space base is "read"`, () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: [], + feature: {}, + }, + { + spaces: ['foo'], + base: ['read'], + feature: {}, + }, + ], + }); + + const privilegeCalculator = buildEffectivePrivileges(role); + + const result: AllowedPrivilege[] = privilegeCalculator.calculateAllowedPrivileges(); + + expect(result).toEqual([ + { + ...unrestrictedBasePrivileges, + ...unrestrictedFeaturePrivileges, + }, + { + base: { + privileges: ['all', 'read'], + canUnassign: true, + }, + feature: { + feature1: { + privileges: ['all', 'read'], + canUnassign: false, + }, + feature2: { + privileges: ['all', 'read'], + canUnassign: false, + }, + feature3: { + privileges: ['all'], + canUnassign: true, // feature 3 has no "read" privilege governed by space "all" + }, + }, + }, + ]); + }); + + it(`allows space base privilege to be set to "all" or "read" when space base is already "all"`, () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['foo'], + base: ['all'], + feature: {}, + }, + ], + }); + + const privilegeCalculator = buildEffectivePrivileges(role); + + const result: AllowedPrivilege[] = privilegeCalculator.calculateAllowedPrivileges(); + + expect(result).toEqual([ + { + ...unrestrictedBasePrivileges, + feature: { + feature1: { + privileges: ['all'], + canUnassign: false, + }, + feature2: { + privileges: ['all'], + canUnassign: false, + }, + feature3: { + privileges: ['all'], + canUnassign: false, + }, + }, + }, + ]); + }); + + it(`restricts space feature privileges when global feature privileges are set`, () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: [], + feature: { + feature1: ['all'], + feature2: ['read'], + }, + }, + { + spaces: ['foo'], + base: [], + feature: {}, + }, + ], + }); + + const privilegeCalculator = buildEffectivePrivileges(role); + + const result: AllowedPrivilege[] = privilegeCalculator.calculateAllowedPrivileges(); + + expect(result).toEqual([ + { + ...unrestrictedBasePrivileges, + ...unrestrictedFeaturePrivileges, + }, + { + base: { + privileges: ['all', 'read'], + canUnassign: true, + }, + feature: { + feature1: { + privileges: ['all'], + canUnassign: false, + }, + feature2: { + privileges: ['all', 'read'], + canUnassign: false, + }, + feature3: { + privileges: ['all'], + canUnassign: true, // feature 3 has no "read" privilege governed by space "all" + }, + }, + }, + ]); + }); +}); diff --git a/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_privilege_calculator.ts b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_privilege_calculator.ts new file mode 100644 index 000000000000..58c371e80290 --- /dev/null +++ b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_privilege_calculator.ts @@ -0,0 +1,114 @@ +/* + * 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 _ from 'lodash'; +import { + FeaturesPrivileges, + KibanaPrivileges, + Role, + RoleKibanaPrivilege, +} from '../../../common/model'; +import { isGlobalPrivilegeDefinition } from '../privilege_utils'; +import { KibanaAllowedPrivilegesCalculator } from './kibana_allowed_privileges_calculator'; +import { KibanaBasePrivilegeCalculator } from './kibana_base_privilege_calculator'; +import { KibanaFeaturePrivilegeCalculator } from './kibana_feature_privilege_calculator'; +import { AllowedPrivilege, CalculatedPrivilege } from './kibana_privilege_calculator_types'; + +export class KibanaPrivilegeCalculator { + private allowedPrivilegesCalculator: KibanaAllowedPrivilegesCalculator; + + private effectiveBasePrivilegesCalculator: KibanaBasePrivilegeCalculator; + + private effectiveFeaturePrivilegesCalculator: KibanaFeaturePrivilegeCalculator; + + constructor( + private readonly kibanaPrivileges: KibanaPrivileges, + private readonly role: Role, + public readonly rankedFeaturePrivileges: FeaturesPrivileges + ) { + const globalPrivilege = this.locateGlobalPrivilege(role); + + const assignedGlobalBaseActions: string[] = globalPrivilege.base[0] + ? kibanaPrivileges.getGlobalPrivileges().getActions(globalPrivilege.base[0]) + : []; + + this.allowedPrivilegesCalculator = new KibanaAllowedPrivilegesCalculator( + kibanaPrivileges, + role + ); + + this.effectiveBasePrivilegesCalculator = new KibanaBasePrivilegeCalculator( + kibanaPrivileges, + globalPrivilege, + assignedGlobalBaseActions + ); + + this.effectiveFeaturePrivilegesCalculator = new KibanaFeaturePrivilegeCalculator( + kibanaPrivileges, + globalPrivilege, + assignedGlobalBaseActions, + rankedFeaturePrivileges + ); + } + + public calculateEffectivePrivileges(ignoreAssigned: boolean = false): CalculatedPrivilege[] { + const { kibana = [] } = this.role; + return kibana.map(privilegeSpec => + this.calculateEffectivePrivilege(privilegeSpec, ignoreAssigned) + ); + } + + public calculateAllowedPrivileges(): AllowedPrivilege[] { + const effectivePrivs = this.calculateEffectivePrivileges(true); + return this.allowedPrivilegesCalculator.calculateAllowedPrivileges(effectivePrivs); + } + + private calculateEffectivePrivilege( + privilegeSpec: RoleKibanaPrivilege, + ignoreAssigned: boolean + ): CalculatedPrivilege { + const result: CalculatedPrivilege = { + base: this.effectiveBasePrivilegesCalculator.getMostPermissiveBasePrivilege( + privilegeSpec, + ignoreAssigned + ), + feature: {}, + reserved: privilegeSpec._reserved, + }; + + // If calculations wish to ignoreAssigned, then we still need to know what the real effective base privilege is + // without ignoring assigned, in order to calculate the correct feature privileges. + const effectiveBase = ignoreAssigned + ? this.effectiveBasePrivilegesCalculator.getMostPermissiveBasePrivilege(privilegeSpec, false) + : result.base; + + const allFeaturePrivileges = this.kibanaPrivileges.getFeaturePrivileges().getAllPrivileges(); + result.feature = Object.keys(allFeaturePrivileges).reduce((acc, featureId) => { + return { + ...acc, + [featureId]: this.effectiveFeaturePrivilegesCalculator.getMostPermissiveFeaturePrivilege( + privilegeSpec, + effectiveBase, + featureId, + ignoreAssigned + ), + }; + }, {}); + + return result; + } + + private locateGlobalPrivilege(role: Role) { + const spacePrivileges = role.kibana; + return ( + spacePrivileges.find(privileges => isGlobalPrivilegeDefinition(privileges)) || { + spaces: [] as string[], + base: [] as string[], + feature: {}, + } + ); + } +} diff --git a/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_privilege_calculator_types.ts b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_privilege_calculator_types.ts new file mode 100644 index 000000000000..41f6012737a9 --- /dev/null +++ b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_privilege_calculator_types.ts @@ -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. + */ + +/** + * Describes the source of a privilege. + */ +export enum PRIVILEGE_SOURCE { + /** Privilege is assigned directly to the entity */ + SPACE_FEATURE = 10, + + /** Privilege is derived from space base privilege */ + SPACE_BASE = 20, + + /** Privilege is derived from global feature privilege */ + GLOBAL_FEATURE = 30, + + /** Privilege is derived from global base privilege */ + GLOBAL_BASE = 40, +} + +export interface PrivilegeExplanation { + actualPrivilege: string; + actualPrivilegeSource: PRIVILEGE_SOURCE; + isDirectlyAssigned: boolean; + supersededPrivilege?: string; + supersededPrivilegeSource?: PRIVILEGE_SOURCE; +} + +export interface CalculatedPrivilege { + base: PrivilegeExplanation; + feature: { + [featureId: string]: PrivilegeExplanation | undefined; + }; + reserved: undefined | string[]; +} + +export interface PrivilegeScenario { + actualPrivilegeSource: PRIVILEGE_SOURCE; + isDirectlyAssigned: boolean; + supersededPrivilege?: string; + supersededPrivilegeSource?: PRIVILEGE_SOURCE; + actions: string[]; +} + +export interface AllowedPrivilege { + base: { + privileges: string[]; + canUnassign: boolean; + }; + feature: { + [featureId: string]: + | { + privileges: string[]; + canUnassign: boolean; + } + | undefined; + }; +} diff --git a/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_privileges_calculator_factory.ts b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_privileges_calculator_factory.ts new file mode 100644 index 000000000000..3d8a0698465a --- /dev/null +++ b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_privileges_calculator_factory.ts @@ -0,0 +1,77 @@ +/* + * 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 _ from 'lodash'; +import { FeaturesPrivileges, KibanaPrivileges, Role } from '../../../common/model'; +import { compareActions } from '../../../common/privilege_calculator_utils'; +import { copyRole } from '../../lib/role_utils'; +import { KibanaPrivilegeCalculator } from './kibana_privilege_calculator'; + +export class KibanaPrivilegeCalculatorFactory { + /** All feature privileges, sorted from most permissive => least permissive. */ + public readonly rankedFeaturePrivileges: FeaturesPrivileges; + + constructor(private readonly kibanaPrivileges: KibanaPrivileges) { + this.rankedFeaturePrivileges = {}; + const featurePrivilegeSet = kibanaPrivileges.getFeaturePrivileges().getAllPrivileges(); + + Object.entries(featurePrivilegeSet).forEach(([featureId, privileges]) => { + this.rankedFeaturePrivileges[featureId] = privileges.sort((privilege1, privilege2) => { + const privilege1Actions = kibanaPrivileges + .getFeaturePrivileges() + .getActions(featureId, privilege1); + const privilege2Actions = kibanaPrivileges + .getFeaturePrivileges() + .getActions(featureId, privilege2); + return compareActions(privilege1Actions, privilege2Actions); + }); + }); + } + + /** + * Creates an KibanaPrivilegeCalculator instance for the specified role. + * @param role + */ + public getInstance(role: Role) { + const roleCopy = copyRole(role); + + this.sortPrivileges(roleCopy); + return new KibanaPrivilegeCalculator( + this.kibanaPrivileges, + roleCopy, + this.rankedFeaturePrivileges + ); + } + + private sortPrivileges(role: Role) { + role.kibana.forEach(privilege => { + privilege.base.sort((privilege1, privilege2) => { + const privilege1Actions = this.kibanaPrivileges + .getSpacesPrivileges() + .getActions(privilege1); + + const privilege2Actions = this.kibanaPrivileges + .getSpacesPrivileges() + .getActions(privilege2); + + return compareActions(privilege1Actions, privilege2Actions); + }); + + Object.entries(privilege.feature).forEach(([featureId, featurePrivs]) => { + featurePrivs.sort((privilege1, privilege2) => { + const privilege1Actions = this.kibanaPrivileges + .getFeaturePrivileges() + .getActions(featureId, privilege1); + + const privilege2Actions = this.kibanaPrivileges + .getFeaturePrivileges() + .getActions(featureId, privilege2); + + return compareActions(privilege1Actions, privilege2Actions); + }); + }); + }); + } +} diff --git a/x-pack/plugins/security/public/lib/privilege_utils.test.ts b/x-pack/plugins/security/public/lib/privilege_utils.test.ts new file mode 100644 index 000000000000..88ac71cbd763 --- /dev/null +++ b/x-pack/plugins/security/public/lib/privilege_utils.test.ts @@ -0,0 +1,85 @@ +/* + * 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 { hasAssignedFeaturePrivileges, isGlobalPrivilegeDefinition } from './privilege_utils'; + +describe('isGlobalPrivilegeDefinition', () => { + it('returns true if no spaces are defined', () => { + expect( + // @ts-ignore + isGlobalPrivilegeDefinition({ + base: [], + feature: {}, + }) + ).toEqual(true); + }); + + it('returns true if spaces is an empty array', () => { + expect( + isGlobalPrivilegeDefinition({ + spaces: [], + base: [], + feature: {}, + }) + ).toEqual(true); + }); + + it('returns true if spaces contains "*"', () => { + expect( + isGlobalPrivilegeDefinition({ + spaces: ['*'], + base: [], + feature: {}, + }) + ).toEqual(true); + }); + + it('returns false if spaces does not contain "*"', () => { + expect( + isGlobalPrivilegeDefinition({ + spaces: ['foo', 'bar'], + base: [], + feature: {}, + }) + ).toEqual(false); + }); +}); + +describe('hasAssignedFeaturePrivileges', () => { + it('returns false if no feature privileges are defined', () => { + expect( + hasAssignedFeaturePrivileges({ + spaces: [], + base: [], + feature: {}, + }) + ).toEqual(false); + }); + + it('returns false if feature privileges are defined but not assigned', () => { + expect( + hasAssignedFeaturePrivileges({ + spaces: [], + base: [], + feature: { + foo: [], + }, + }) + ).toEqual(false); + }); + + it('returns true if feature privileges are defined and assigned', () => { + expect( + hasAssignedFeaturePrivileges({ + spaces: [], + base: [], + feature: { + foo: ['all'], + }, + }) + ).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security/public/lib/privilege_utils.ts b/x-pack/plugins/security/public/lib/privilege_utils.ts new file mode 100644 index 000000000000..74bde71dc421 --- /dev/null +++ b/x-pack/plugins/security/public/lib/privilege_utils.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RoleKibanaPrivilege } from '../../common/model'; + +/** + * Determines if the passed privilege spec defines global privileges. + * @param privilegeSpec + */ +export function isGlobalPrivilegeDefinition(privilegeSpec: RoleKibanaPrivilege): boolean { + if (!privilegeSpec.spaces || privilegeSpec.spaces.length === 0) { + return true; + } + return privilegeSpec.spaces.includes('*'); +} + +/** + * Determines if the passed privilege spec defines feature privileges. + * @param privilegeSpec + */ +export function hasAssignedFeaturePrivileges(privilegeSpec: RoleKibanaPrivilege): boolean { + const featureKeys = Object.keys(privilegeSpec.feature); + return featureKeys.length > 0 && featureKeys.some(key => privilegeSpec.feature[key].length > 0); +} diff --git a/x-pack/plugins/security/public/lib/role.test.ts b/x-pack/plugins/security/public/lib/role_utils.test.ts similarity index 51% rename from x-pack/plugins/security/public/lib/role.test.ts rename to x-pack/plugins/security/public/lib/role_utils.test.ts index c86b250e034f..706d74b09c03 100644 --- a/x-pack/plugins/security/public/lib/role.test.ts +++ b/x-pack/plugins/security/public/lib/role_utils.test.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isReservedRole, isRoleEnabled } from './role'; +import { Role } from '../../common/model'; +import { copyRole, isReadOnlyRole, isReservedRole, isRoleEnabled } from './role_utils'; describe('role', () => { describe('isRoleEnabled', () => { @@ -56,4 +57,64 @@ describe('role', () => { expect(isReservedRole(testRole)).toBe(false); }); }); + + describe('isReadOnlyRole', () => { + test('returns true for reserved roles', () => { + const testRole = { + metadata: { + _reserved: true, + }, + }; + expect(isReadOnlyRole(testRole)).toBe(true); + }); + + test('returns true for roles with transform errors', () => { + const testRole = { + _transform_error: ['kibana'], + }; + expect(isReadOnlyRole(testRole)).toBe(true); + }); + + test('returns false for all other roles', () => { + const testRole = {}; + expect(isReadOnlyRole(testRole)).toBe(false); + }); + }); + + describe('copyRole', () => { + it('should perform a deep copy', () => { + const role: Role = { + name: '', + elasticsearch: { + cluster: ['all'], + indices: [{ names: ['index*'], privileges: ['all'] }], + run_as: ['user'], + }, + kibana: [ + { + spaces: ['*'], + base: ['all'], + feature: {}, + }, + { + spaces: ['default'], + base: ['foo'], + feature: {}, + }, + { + spaces: ['marketing'], + base: ['read'], + feature: {}, + }, + ], + }; + + const result = copyRole(role); + expect(result).toEqual(role); + + role.elasticsearch.indices[0].names = ['something else']; + + expect(result).not.toEqual(role); + }); + }); }); diff --git a/x-pack/plugins/security/public/lib/role.ts b/x-pack/plugins/security/public/lib/role_utils.ts similarity index 59% rename from x-pack/plugins/security/public/lib/role.ts rename to x-pack/plugins/security/public/lib/role_utils.ts index d6221f7aecb4..471a3b2ba112 100644 --- a/x-pack/plugins/security/public/lib/role.ts +++ b/x-pack/plugins/security/public/lib/role_utils.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get } from 'lodash'; -import { Role } from '../../common/model/role'; +import { cloneDeep, get } from 'lodash'; +import { Role } from '../../common/model'; /** * Returns whether given role is enabled or not @@ -25,3 +25,21 @@ export function isRoleEnabled(role: Partial) { export function isReservedRole(role: Partial) { return get(role, 'metadata._reserved', false); } + +/** + * 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 && role._transform_error.length > 0); +} + +/** + * Returns a deep copy of the role. + * + * @param role the Role to copy. + */ +export function copyRole(role: Role) { + return cloneDeep(role); +} diff --git a/x-pack/plugins/security/public/lib/transform_role_for_save.test.ts b/x-pack/plugins/security/public/lib/transform_role_for_save.test.ts new file mode 100644 index 000000000000..1ea19f263730 --- /dev/null +++ b/x-pack/plugins/security/public/lib/transform_role_for_save.test.ts @@ -0,0 +1,484 @@ +/* + * 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 { Role } from '../../common/model'; +import { transformRoleForSave } from './transform_role_for_save'; + +describe('transformRoleForSave', () => { + describe('spaces disabled', () => { + it('removes placeholder index privileges', () => { + const role: Role = { + name: 'my role', + elasticsearch: { + cluster: [], + indices: [{ names: [], privileges: [] }], + run_as: [], + }, + kibana: [], + }; + + const result = transformRoleForSave(role, false); + + expect(result).toEqual({ + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + }); + }); + + it('removes placeholder query entries', () => { + const role: Role = { + name: 'my role', + elasticsearch: { + cluster: [], + indices: [{ names: ['.kibana*'], privileges: ['all'], query: '' }], + run_as: [], + }, + kibana: [], + }; + + const result = transformRoleForSave(role, false); + + expect(result).toEqual({ + elasticsearch: { + cluster: [], + indices: [{ names: ['.kibana*'], privileges: ['all'] }], + run_as: [], + }, + kibana: [], + }); + }); + + it('removes transient fields not required for save', () => { + const role: Role = { + name: 'my role', + transient_metadata: { + foo: 'bar', + }, + _transform_error: ['kibana'], + metadata: { + someOtherMetadata: true, + }, + _unrecognized_applications: ['foo'], + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + }; + + const result = transformRoleForSave(role, false); + + expect(result).toEqual({ + metadata: { + someOtherMetadata: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + }); + }); + + it('does not remove actual query entries', () => { + const role: Role = { + name: 'my role', + elasticsearch: { + cluster: [], + indices: [{ names: ['.kibana*'], privileges: ['all'], query: 'something' }], + run_as: [], + }, + kibana: [], + }; + + const result = transformRoleForSave(role, false); + + expect(result).toEqual({ + elasticsearch: { + cluster: [], + indices: [{ names: ['.kibana*'], privileges: ['all'], query: 'something' }], + run_as: [], + }, + kibana: [], + }); + }); + + it('should remove feature privileges if a corresponding base privilege is defined', () => { + const role: Role = { + name: 'my role', + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + spaces: ['*'], + base: ['all'], + feature: { + feature1: ['read'], + feature2: ['write'], + }, + }, + ], + }; + + const result = transformRoleForSave(role, false); + + expect(result).toEqual({ + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + spaces: ['*'], + base: ['all'], + feature: {}, + }, + ], + }); + }); + + it('should not remove feature privileges if a corresponding base privilege is not defined', () => { + const role: Role = { + name: 'my role', + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + spaces: ['*'], + base: [], + feature: { + feature1: ['read'], + feature2: ['write'], + }, + }, + ], + }; + + const result = transformRoleForSave(role, false); + + expect(result).toEqual({ + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + spaces: ['*'], + base: [], + feature: { + feature1: ['read'], + feature2: ['write'], + }, + }, + ], + }); + }); + + it('should remove space privileges', () => { + const role: Role = { + name: 'my role', + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + spaces: ['*'], + base: [], + feature: { + feature1: ['read'], + feature2: ['write'], + }, + }, + { + spaces: ['marketing'], + base: [], + feature: { + feature1: ['read'], + feature2: ['write'], + }, + }, + ], + }; + + const result = transformRoleForSave(role, false); + + expect(result).toEqual({ + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + spaces: ['*'], + base: [], + feature: { + feature1: ['read'], + feature2: ['write'], + }, + }, + ], + }); + }); + }); + + describe('spaces enabled', () => { + it('removes placeholder index privileges', () => { + const role: Role = { + name: 'my role', + elasticsearch: { + cluster: [], + indices: [{ names: [], privileges: [] }], + run_as: [], + }, + kibana: [], + }; + + const result = transformRoleForSave(role, true); + + expect(result).toEqual({ + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + }); + }); + + it('removes placeholder query entries', () => { + const role: Role = { + name: 'my role', + elasticsearch: { + cluster: [], + indices: [{ names: ['.kibana*'], privileges: ['all'], query: '' }], + run_as: [], + }, + kibana: [], + }; + + const result = transformRoleForSave(role, true); + + expect(result).toEqual({ + elasticsearch: { + cluster: [], + indices: [{ names: ['.kibana*'], privileges: ['all'] }], + run_as: [], + }, + kibana: [], + }); + }); + + it('removes transient fields not required for save', () => { + const role: Role = { + name: 'my role', + transient_metadata: { + foo: 'bar', + }, + _transform_error: ['kibana'], + metadata: { + someOtherMetadata: true, + }, + _unrecognized_applications: ['foo'], + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + }; + + const result = transformRoleForSave(role, true); + + expect(result).toEqual({ + metadata: { + someOtherMetadata: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + }); + }); + + it('does not remove actual query entries', () => { + const role: Role = { + name: 'my role', + elasticsearch: { + cluster: [], + indices: [{ names: ['.kibana*'], privileges: ['all'], query: 'something' }], + run_as: [], + }, + kibana: [], + }; + + const result = transformRoleForSave(role, true); + + expect(result).toEqual({ + elasticsearch: { + cluster: [], + indices: [{ names: ['.kibana*'], privileges: ['all'], query: 'something' }], + run_as: [], + }, + kibana: [], + }); + }); + + it('should remove feature privileges if a corresponding base privilege is defined', () => { + const role: Role = { + name: 'my role', + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + spaces: ['foo'], + base: ['all'], + feature: { + feature1: ['read'], + feature2: ['write'], + }, + }, + ], + }; + + const result = transformRoleForSave(role, true); + + expect(result).toEqual({ + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + spaces: ['foo'], + base: ['all'], + feature: {}, + }, + ], + }); + }); + + it('should not remove feature privileges if a corresponding base privilege is not defined', () => { + const role: Role = { + name: 'my role', + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + spaces: ['foo'], + base: [], + feature: { + feature1: ['read'], + feature2: ['write'], + }, + }, + ], + }; + + const result = transformRoleForSave(role, true); + + expect(result).toEqual({ + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + spaces: ['foo'], + base: [], + feature: { + feature1: ['read'], + feature2: ['write'], + }, + }, + ], + }); + }); + + it('should not remove space privileges', () => { + const role: Role = { + name: 'my role', + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + spaces: ['*'], + base: [], + feature: { + feature1: ['read'], + feature2: ['write'], + }, + }, + { + spaces: ['marketing'], + base: [], + feature: { + feature1: ['read'], + feature2: ['write'], + }, + }, + ], + }; + + const result = transformRoleForSave(role, true); + + expect(result).toEqual({ + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + spaces: ['*'], + base: [], + feature: { + feature1: ['read'], + feature2: ['write'], + }, + }, + { + spaces: ['marketing'], + base: [], + feature: { + feature1: ['read'], + feature2: ['write'], + }, + }, + ], + }); + }); + }); +}); diff --git a/x-pack/plugins/security/public/lib/transform_role_for_save.ts b/x-pack/plugins/security/public/lib/transform_role_for_save.ts new file mode 100644 index 000000000000..861ba530050a --- /dev/null +++ b/x-pack/plugins/security/public/lib/transform_role_for_save.ts @@ -0,0 +1,41 @@ +/* + * 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 { Role, RoleIndexPrivilege } from '../../common/model'; +import { isGlobalPrivilegeDefinition } from './privilege_utils'; + +export function transformRoleForSave(role: Role, spacesEnabled: boolean) { + // Remove any placeholder index privileges + role.elasticsearch.indices = role.elasticsearch.indices.filter( + indexPrivilege => !isPlaceholderPrivilege(indexPrivilege) + ); + + // Remove any placeholder query entries + role.elasticsearch.indices.forEach(index => index.query || delete index.query); + + // If spaces are disabled, then do not persist any space privileges + if (!spacesEnabled) { + role.kibana = role.kibana.filter(isGlobalPrivilegeDefinition); + } + + role.kibana.forEach(kibanaPrivilege => { + // If a base privilege is defined, then do not persist feature privileges + if (kibanaPrivilege.base.length > 0) { + kibanaPrivilege.feature = {}; + } + }); + + delete role.name; + delete role.transient_metadata; + delete role._unrecognized_applications; + delete role._transform_error; + + return role; +} + +function isPlaceholderPrivilege(indexPrivilege: RoleIndexPrivilege) { + return indexPrivilege.names.length === 0; +} diff --git a/x-pack/plugins/security/public/objects/lib/roles.ts b/x-pack/plugins/security/public/objects/lib/roles.ts index 2551d7eabc4e..e33cbe4c6c03 100644 --- a/x-pack/plugins/security/public/objects/lib/roles.ts +++ b/x-pack/plugins/security/public/objects/lib/roles.ts @@ -3,14 +3,16 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { omit } from 'lodash'; import chrome from 'ui/chrome'; -import { Role } from '../../../common/model/role'; +import { Role } from '../../../common/model'; +import { copyRole } from '../../lib/role_utils'; +import { transformRoleForSave } from '../../lib/transform_role_for_save'; const apiBase = chrome.addBasePath(`/api/security/role`); -export async function saveRole($http: any, role: Role) { - const data = omit(role, 'name', 'transient_metadata', '_unrecognized_applications'); +export async function saveRole($http: any, role: Role, spacesEnabled: boolean) { + const data = transformRoleForSave(copyRole(role), spacesEnabled); + return await $http.put(`${apiBase}/${role.name}`, data); } diff --git a/x-pack/plugins/security/public/views/_index.scss b/x-pack/plugins/security/public/views/_index.scss new file mode 100644 index 000000000000..fa95d44f637e --- /dev/null +++ b/x-pack/plugins/security/public/views/_index.scss @@ -0,0 +1,8 @@ +// Public views +@import './logged_out/index'; + +// Login styles +@import './login/index'; + +// Management styles +@import './management/index'; diff --git a/x-pack/plugins/security/public/views/management/_index.scss b/x-pack/plugins/security/public/views/management/_index.scss new file mode 100644 index 000000000000..d1064f589573 --- /dev/null +++ b/x-pack/plugins/security/public/views/management/_index.scss @@ -0,0 +1,2 @@ +@import './change_password_form/index'; +@import './edit_role/index'; diff --git a/x-pack/plugins/security/public/views/management/change_password_form/_change_password_form.scss b/x-pack/plugins/security/public/views/management/change_password_form/_change_password_form.scss new file mode 100644 index 000000000000..98331c2070a3 --- /dev/null +++ b/x-pack/plugins/security/public/views/management/change_password_form/_change_password_form.scss @@ -0,0 +1,17 @@ +.secChangePasswordForm__panel { + max-width: $secFormWidth; +} + +.secChangePasswordForm__subLabel { + margin-bottom: $euiSizeS; +} + +.secChangePasswordForm__footer { + display: flex; + justify-content: flex-start; + align-items: center; + + .kuiButton + .kuiButton { + margin-left: $euiSizeS; + } +} diff --git a/x-pack/plugins/security/public/views/management/change_password_form/_index.scss b/x-pack/plugins/security/public/views/management/change_password_form/_index.scss new file mode 100644 index 000000000000..a6058b5ddebb --- /dev/null +++ b/x-pack/plugins/security/public/views/management/change_password_form/_index.scss @@ -0,0 +1 @@ +@import './change_password_form'; diff --git a/x-pack/plugins/security/public/views/management/change_password_form/change_password_form.html b/x-pack/plugins/security/public/views/management/change_password_form/change_password_form.html index f5f717f27e9c..92fb95861a6f 100644 --- a/x-pack/plugins/security/public/views/management/change_password_form/change_password_form.html +++ b/x-pack/plugins/security/public/views/management/change_password_form/change_password_form.html @@ -117,7 +117,7 @@
- diff --git a/x-pack/plugins/spaces/public/views/nav_control/components/spaces_description.test.tsx b/x-pack/plugins/spaces/public/views/nav_control/components/spaces_description.test.tsx index d8aca1a9a483..4a2fd3466586 100644 --- a/x-pack/plugins/spaces/public/views/nav_control/components/spaces_description.test.tsx +++ b/x-pack/plugins/spaces/public/views/nav_control/components/spaces_description.test.tsx @@ -10,13 +10,6 @@ import { SpacesDescription } from './spaces_description'; describe('SpacesDescription', () => { it('renders without crashing', () => { - expect( - shallow( - true }} - onManageSpacesClick={jest.fn()} - /> - ) - ).toMatchSnapshot(); + expect(shallow()).toMatchSnapshot(); }); }); diff --git a/x-pack/plugins/spaces/public/views/nav_control/components/spaces_description.tsx b/x-pack/plugins/spaces/public/views/nav_control/components/spaces_description.tsx index 9ac22c21c280..fe071dba2546 100644 --- a/x-pack/plugins/spaces/public/views/nav_control/components/spaces_description.tsx +++ b/x-pack/plugins/spaces/public/views/nav_control/components/spaces_description.tsx @@ -6,12 +6,10 @@ import { EuiContextMenuPanel, EuiText } from '@elastic/eui'; import React, { SFC } from 'react'; -import { UserProfile } from '../../../../../xpack_main/public/services/user_profile'; import { ManageSpacesButton } from '../../../components'; import { getSpacesFeatureDescription } from '../../../lib/constants'; interface Props { - userProfile: UserProfile; onManageSpacesClick: () => void; } @@ -30,7 +28,6 @@ export const SpacesDescription: SFC = (props: Props) => {
diff --git a/x-pack/plugins/spaces/public/views/nav_control/components/spaces_menu.tsx b/x-pack/plugins/spaces/public/views/nav_control/components/spaces_menu.tsx index 825a14e7928b..76a47ca73862 100644 --- a/x-pack/plugins/spaces/public/views/nav_control/components/spaces_menu.tsx +++ b/x-pack/plugins/spaces/public/views/nav_control/components/spaces_menu.tsx @@ -7,7 +7,6 @@ import { EuiContextMenuItem, EuiContextMenuPanel, EuiFieldSearch, EuiText } from '@elastic/eui'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { Component } from 'react'; -import { UserProfile } from '../../../../../xpack_main/public/services/user_profile'; import { SPACE_SEARCH_COUNT_THRESHOLD } from '../../../../common/constants'; import { Space } from '../../../../common/model/space'; import { ManageSpacesButton, SpaceAvatar } from '../../../components'; @@ -16,7 +15,6 @@ interface Props { spaces: Space[]; onSelectSpace: (space: Space) => void; onManageSpacesClick: () => void; - userProfile: UserProfile; intl: InjectedIntl; } @@ -152,7 +150,6 @@ class SpacesMenuUI extends Component { key="manageSpacesButton" className="spcMenu__manageButton" size="s" - userProfile={this.props.userProfile} onClick={this.props.onManageSpacesClick} /> ); diff --git a/x-pack/plugins/spaces/public/views/nav_control/nav_control.tsx b/x-pack/plugins/spaces/public/views/nav_control/nav_control.tsx index 6385e74280a1..b07c30f8d7bd 100644 --- a/x-pack/plugins/spaces/public/views/nav_control/nav_control.tsx +++ b/x-pack/plugins/spaces/public/views/nav_control/nav_control.tsx @@ -11,7 +11,6 @@ import template from 'plugins/spaces/views/nav_control/nav_control.html'; import { NavControlPopover } from 'plugins/spaces/views/nav_control/nav_control_popover'; // @ts-ignore import { PathProvider } from 'plugins/xpack_main/services/path'; -import { UserProfileProvider } from 'plugins/xpack_main/services/user_profile'; import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import ReactDOM from 'react-dom'; @@ -46,7 +45,6 @@ let spacesManager: SpacesManager; module.controller( 'spacesNavController', ($scope: any, $http: any, chrome: any, Private: any, activeSpace: any) => { - const userProfile = Private(UserProfileProvider); const pathProvider = Private(PathProvider); const domNode = document.getElementById(`spacesNavReactRoot`); @@ -63,7 +61,6 @@ module.controller( @@ -103,7 +100,6 @@ chromeHeaderNavControlsRegistry.register( order: 1000, side: NavControlSide.Left, render(el: HTMLElement) { - const userProfile = Private(UserProfileProvider); const pathProvider = Private(PathProvider); if (pathProvider.isUnauthenticated()) { @@ -119,7 +115,6 @@ chromeHeaderNavControlsRegistry.register( diff --git a/x-pack/plugins/spaces/public/views/nav_control/nav_control_popover.test.tsx b/x-pack/plugins/spaces/public/views/nav_control/nav_control_popover.test.tsx index a1088ba91801..f67cc4076263 100644 --- a/x-pack/plugins/spaces/public/views/nav_control/nav_control_popover.test.tsx +++ b/x-pack/plugins/spaces/public/views/nav_control/nav_control_popover.test.tsx @@ -20,10 +20,12 @@ const createMockHttpAgent = (withSpaces = false) => { { id: '', name: 'space 1', + disabledFeatures: [], }, { id: '', name: 'space 2', + disabledFeatures: [], }, ]; @@ -42,7 +44,7 @@ const createMockHttpAgent = (withSpaces = false) => { describe('NavControlPopover', () => { it('renders without crashing', () => { const activeSpace = { - space: { id: '', name: 'foo' }, + space: { id: '', name: 'foo', disabledFeatures: [] }, valid: true, }; @@ -52,7 +54,6 @@ describe('NavControlPopover', () => { true }} anchorPosition={'downRight'} buttonClass={SpacesGlobalNavButton} /> @@ -62,7 +63,7 @@ describe('NavControlPopover', () => { it('renders a SpaceAvatar with the active space', async () => { const activeSpace = { - space: { id: '', name: 'foo' }, + space: { id: '', name: 'foo', disabledFeatures: [] }, valid: true, }; @@ -74,7 +75,6 @@ describe('NavControlPopover', () => { true }} anchorPosition={'rightCenter'} buttonClass={SpacesGlobalNavButton} /> diff --git a/x-pack/plugins/spaces/public/views/nav_control/nav_control_popover.tsx b/x-pack/plugins/spaces/public/views/nav_control/nav_control_popover.tsx index 4ae7319b4d54..4669bd3608e4 100644 --- a/x-pack/plugins/spaces/public/views/nav_control/nav_control_popover.tsx +++ b/x-pack/plugins/spaces/public/views/nav_control/nav_control_popover.tsx @@ -6,7 +6,6 @@ import { EuiAvatar, EuiPopover, PopoverAnchorPosition } from '@elastic/eui'; import React, { Component, ComponentClass } from 'react'; -import { UserProfile } from '../../../../xpack_main/public/services/user_profile'; import { Space } from '../../../common/model/space'; import { SpaceAvatar } from '../../components'; import { SpacesManager } from '../../lib/spaces_manager'; @@ -21,7 +20,6 @@ interface Props { error?: string; space: Space; }; - userProfile: UserProfile; anchorPosition: PopoverAnchorPosition; buttonClass: ComponentClass; } @@ -62,18 +60,12 @@ export class NavControlPopover extends Component { let element: React.ReactNode; if (this.state.spaces.length < 2) { - element = ( - - ); + element = ; } else { element = ( ); diff --git a/x-pack/plugins/spaces/public/views/space_selector/space_selector.test.tsx b/x-pack/plugins/spaces/public/views/space_selector/space_selector.test.tsx index f12b5b8ab8e1..5869ac9f7989 100644 --- a/x-pack/plugins/spaces/public/views/space_selector/space_selector.test.tsx +++ b/x-pack/plugins/spaces/public/views/space_selector/space_selector.test.tsx @@ -49,6 +49,7 @@ test('it uses the spaces on props, when provided', () => { id: 'space-1', name: 'Space 1', description: 'This is the first space', + disabledFeatures: [], }, ]; @@ -72,6 +73,7 @@ test('it queries for spaces when not provided on props', () => { id: 'space-1', name: 'Space 1', description: 'This is the first space', + disabledFeatures: [], }, ]; diff --git a/x-pack/plugins/spaces/server/lib/create_default_space.test.ts b/x-pack/plugins/spaces/server/lib/create_default_space.test.ts index a71278a73718..11ab5e71a0ea 100644 --- a/x-pack/plugins/spaces/server/lib/create_default_space.test.ts +++ b/x-pack/plugins/spaces/server/lib/create_default_space.test.ts @@ -99,6 +99,7 @@ test(`it creates the default space when one does not exist`, async () => { { _reserved: true, description: 'This is your default space!', + disabledFeatures: [], name: 'Default', color: '#00bfb3', }, diff --git a/x-pack/plugins/spaces/server/lib/create_default_space.ts b/x-pack/plugins/spaces/server/lib/create_default_space.ts index f4ade5847892..fdcdbb1ef72b 100644 --- a/x-pack/plugins/spaces/server/lib/create_default_space.ts +++ b/x-pack/plugins/spaces/server/lib/create_default_space.ts @@ -40,6 +40,7 @@ export async function createDefaultSpace(server: any) { defaultMessage: 'This is your default space!', }), color: '#00bfb3', + disabledFeatures: [], _reserved: true, }, options diff --git a/x-pack/plugins/spaces/server/lib/migrations/index.ts b/x-pack/plugins/spaces/server/lib/migrations/index.ts new file mode 100644 index 000000000000..b303a8489ffb --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/migrations/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 { migrateToKibana660 } from './migrate_6x'; diff --git a/x-pack/plugins/spaces/server/lib/migrations/migrate_6x.test.ts b/x-pack/plugins/spaces/server/lib/migrations/migrate_6x.test.ts new file mode 100644 index 000000000000..964eb8137685 --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/migrations/migrate_6x.test.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { migrateToKibana660 } from './migrate_6x'; + +describe('migrateTo660', () => { + it('adds a "disabledFeatures" attribute initialized as an empty array', () => { + expect( + migrateToKibana660({ + id: 'space:foo', + attributes: {}, + }) + ).toEqual({ + id: 'space:foo', + attributes: { + disabledFeatures: [], + }, + }); + }); + + it('does not initialize "disabledFeatures" if the property already exists', () => { + // This scenario shouldn't happen organically. Protecting against defects in the migration. + expect( + migrateToKibana660({ + id: 'space:foo', + attributes: { + disabledFeatures: ['foo', 'bar', 'baz'], + }, + }) + ).toEqual({ + id: 'space:foo', + attributes: { + disabledFeatures: ['foo', 'bar', 'baz'], + }, + }); + }); +}); diff --git a/x-pack/plugins/spaces/server/lib/migrations/migrate_6x.ts b/x-pack/plugins/spaces/server/lib/migrations/migrate_6x.ts new file mode 100644 index 000000000000..0c080a8dabb0 --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/migrations/migrate_6x.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export function migrateToKibana660(doc: Record) { + if (!doc.attributes.hasOwnProperty('disabledFeatures')) { + doc.attributes.disabledFeatures = []; + } + return doc; +} diff --git a/x-pack/plugins/spaces/server/lib/request_inteceptors/index.ts b/x-pack/plugins/spaces/server/lib/request_inteceptors/index.ts new file mode 100644 index 000000000000..2687a2c16295 --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/request_inteceptors/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { initSpacesOnPostAuthRequestInterceptor } from './on_post_auth_interceptor'; +import { initSpacesOnRequestInterceptor } from './on_request_interceptor'; + +export function initSpacesRequestInterceptors(server: any) { + initSpacesOnRequestInterceptor(server); + initSpacesOnPostAuthRequestInterceptor(server); +} diff --git a/x-pack/plugins/spaces/server/lib/request_inteceptors/on_post_auth_interceptor.test.ts b/x-pack/plugins/spaces/server/lib/request_inteceptors/on_post_auth_interceptor.test.ts new file mode 100644 index 000000000000..0e6585ee4892 --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/request_inteceptors/on_post_auth_interceptor.test.ts @@ -0,0 +1,404 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Server } from 'hapi'; +import sinon from 'sinon'; + +import { SavedObject } from 'src/legacy/server/saved_objects'; +import { Feature } from '../../../../xpack_main/types'; +import { convertSavedObjectToSpace } from '../../routes/lib'; +import { initSpacesOnPostAuthRequestInterceptor } from './on_post_auth_interceptor'; +import { initSpacesOnRequestInterceptor } from './on_request_interceptor'; + +describe('onPostAuthRequestInterceptor', () => { + const sandbox = sinon.sandbox.create(); + const teardowns: Array<() => void> = []; + const headers = { + authorization: 'foo', + }; + let server: any; + let request: any; + + const serverBasePath = '/'; + const defaultRoute = '/app/custom-app'; + + beforeEach(() => { + teardowns.push(() => sandbox.restore()); + request = async ( + path: string, + spaces: SavedObject[], + setupFn: (server: Server) => null = () => null + ) => { + server = new Server(); + + interface Config { + [key: string]: any; + } + const config: Config = { + 'server.basePath': serverBasePath, + 'server.defaultRoute': defaultRoute, + }; + + server.decorate( + 'server', + 'config', + jest.fn(() => { + return { + get: jest.fn(key => { + return config[key]; + }), + }; + }) + ); + + server.savedObjects = { + SavedObjectsClient: { + errors: { + isNotFoundError: (e: Error) => e.message === 'space not found', + }, + }, + getSavedObjectsRepository: jest.fn().mockImplementation(() => { + return { + get: (type: string, id: string) => { + if (type === 'space') { + const space = spaces.find(s => s.id === id); + if (space) { + return space; + } + throw new Error('space not found'); + } + }, + create: () => null, + }; + }), + }; + + server.plugins = { + spaces: { + spacesClient: { + getScopedClient: jest.fn(), + }, + }, + xpack_main: { + getFeatures: () => + [ + { + id: 'feature-1', + name: 'feature 1', + app: ['app-1'], + }, + { + id: 'feature-2', + name: 'feature 2', + app: ['app-2'], + }, + { + id: 'feature-4', + name: 'feature 4', + app: ['app-1', 'app-4'], + }, + { + id: 'feature-5', + name: 'feature 4', + app: ['kibana'], + }, + ] as Feature[], + }, + }; + + let basePath: string | undefined; + server.decorate('request', 'getBasePath', () => basePath); + server.decorate('request', 'setBasePath', (newPath: string) => { + basePath = newPath; + }); + + // The onRequest interceptor is also included here because the onPostAuth interceptor requires the onRequest + // interceptor to parse out the space id and rewrite the request's URL. Rather than duplicating that logic, + // we are including the already tested interceptor here in the test chain. + initSpacesOnRequestInterceptor(server); + initSpacesOnPostAuthRequestInterceptor(server); + + server.route([ + { + method: 'GET', + path: '/', + handler: (req: any) => { + return { path: req.path, url: req.url, basePath: req.getBasePath() }; + }, + }, + { + method: 'GET', + path: '/app/{appId}', + handler: (req: any) => { + return { path: req.path, url: req.url, basePath: req.getBasePath() }; + }, + }, + { + method: 'GET', + path: '/api/foo', + handler: (req: any) => { + return { path: req.path, url: req.url, basePath: req.getBasePath() }; + }, + }, + ]); + + teardowns.push(() => server.stop()); + + server.plugins.spaces.spacesClient.getScopedClient.mockReturnValue({ + getAll() { + return spaces.map(convertSavedObjectToSpace); + }, + get(spaceId: string) { + const space = spaces.find(s => s.id === spaceId); + if (!space) { + throw new Error('space not found'); + } + return convertSavedObjectToSpace(space); + }, + }); + + await setupFn(server); + + return await server.inject({ + method: 'GET', + url: path, + headers, + }); + }; + }); + + afterEach(async () => { + await Promise.all(teardowns.splice(0).map(fn => fn())); + }); + + describe('when accessing an app within a non-existent space', () => { + it('redirects to the space selector screen', async () => { + const spaces = [ + { + id: 'a-space', + type: 'space', + attributes: { + name: 'a space', + }, + }, + ]; + + const response = await request('/s/not-found/app/kibana', spaces); + + expect(response.statusCode).toEqual(302); + expect(response.headers.location).toEqual(serverBasePath); + }); + }); + + it('when accessing the kibana app it always allows the request to continue', async () => { + const spaces = [ + { + id: 'a-space', + type: 'space', + attributes: { + name: 'a space', + disabledFeatures: ['feature-1', 'feature-2', 'feature-4', 'feature-5'], + }, + }, + ]; + + const response = await request('/s/a-space/app/kibana', spaces); + + expect(response.statusCode).toEqual(200); + }); + + describe('when accessing an API endpoint within a non-existent space', () => { + it('allows the request to continue', async () => { + const spaces = [ + { + id: 'a-space', + type: 'space', + attributes: { + name: 'a space', + }, + }, + ]; + + const response = await request('/s/not-found/api/foo', spaces); + + expect(response.statusCode).toEqual(200); + }); + }); + + describe('with a single available space', () => { + test('it redirects to the defaultRoute within the context of the single Space when navigating to Kibana root', async () => { + const spaces = [ + { + id: 'a-space', + type: 'space', + attributes: { + name: 'a space', + }, + }, + ]; + + const response = await request('/', spaces); + + expect(response.statusCode).toEqual(302); + expect(response.headers.location).toEqual(`${serverBasePath}/s/a-space${defaultRoute}`); + + expect(server.plugins.spaces.spacesClient.getScopedClient).toHaveBeenCalledWith( + expect.objectContaining({ + headers: expect.objectContaining({ + authorization: headers.authorization, + }), + }) + ); + }); + + test('it redirects to the defaultRoute within the context of the Default Space when navigating to Kibana root', async () => { + // This is very similar to the test above, but this handles the condition where the only available space is the Default Space, + // which does not have a URL Context. In this scenario, the end result is the same as the other test, but the final URL the user + // is redirected to does not contain a space identifier (e.g., /s/foo) + + const spaces = [ + { + id: 'default', + type: 'space', + attributes: { + name: 'Default Space', + }, + }, + ]; + + const response = await request('/', spaces); + + expect(response.statusCode).toEqual(302); + expect(response.headers.location).toEqual(`${serverBasePath}${defaultRoute}`); + expect(server.plugins.spaces.spacesClient.getScopedClient).toHaveBeenCalledWith( + expect.objectContaining({ + headers: expect.objectContaining({ + authorization: headers.authorization, + }), + }) + ); + }); + + test('it allows navigation to apps when none are disabled', async () => { + const spaces = [ + { + id: 'a-space', + type: 'space', + attributes: { + name: 'a space', + disabledFeatures: [] as any, + }, + }, + ]; + + const response = await request('/s/a-space/app/kibana', spaces); + + expect(response.statusCode).toEqual(200); + + expect(server.plugins.spaces.spacesClient.getScopedClient).toHaveBeenCalledWith( + expect.objectContaining({ + headers: expect.objectContaining({ + authorization: headers.authorization, + }), + }) + ); + }); + + test('allows navigation to app that is granted by multiple features, when only one of those features is disabled', async () => { + const spaces = [ + { + id: 'a-space', + type: 'space', + attributes: { + name: 'a space', + disabledFeatures: ['feature-1'] as any, + }, + }, + ]; + + const response = await request('/s/a-space/app/app-1', spaces); + + expect(response.statusCode).toEqual(200); + + expect(server.plugins.spaces.spacesClient.getScopedClient).toHaveBeenCalledWith( + expect.objectContaining({ + headers: expect.objectContaining({ + authorization: headers.authorization, + }), + }) + ); + }); + + test('does not allow navigation to apps that are only provided by a disabled feature', async () => { + const spaces = [ + { + id: 'a-space', + type: 'space', + attributes: { + name: 'a space', + disabledFeatures: ['feature-2'] as any, + }, + }, + ]; + + const response = await request('/s/a-space/app/app-2', spaces); + + expect(response.statusCode).toEqual(404); + + expect(server.plugins.spaces.spacesClient.getScopedClient).toHaveBeenCalledWith( + expect.objectContaining({ + headers: expect.objectContaining({ + authorization: headers.authorization, + }), + }) + ); + }); + }); + + describe('with multiple available spaces', () => { + test('it redirects to the Space Selector App when navigating to Kibana root', async () => { + const spaces = [ + { + id: 'a-space', + type: 'space', + attributes: { + name: 'a space', + }, + }, + { + id: 'b-space', + type: 'space', + attributes: { + name: 'b space', + }, + }, + ]; + + const getHiddenUiAppHandler = jest.fn(() => '
space selector
'); + + const response = await request('/', spaces, function setupFn() { + server.decorate('server', 'getHiddenUiAppById', getHiddenUiAppHandler); + server.decorate('toolkit', 'renderApp', function renderAppHandler(app: any) { + // @ts-ignore + return this.response(app); + }); + }); + + expect(response.statusCode).toEqual(200); + expect(response.payload).toEqual('
space selector
'); + + expect(getHiddenUiAppHandler).toHaveBeenCalledTimes(1); + expect(getHiddenUiAppHandler).toHaveBeenCalledWith('space_selector'); + expect(server.plugins.spaces.spacesClient.getScopedClient).toHaveBeenCalledWith( + expect.objectContaining({ + headers: expect.objectContaining({ + authorization: headers.authorization, + }), + }) + ); + }); + }); +}); diff --git a/x-pack/plugins/spaces/server/lib/space_request_interceptors.ts b/x-pack/plugins/spaces/server/lib/request_inteceptors/on_post_auth_interceptor.ts similarity index 52% rename from x-pack/plugins/spaces/server/lib/space_request_interceptors.ts rename to x-pack/plugins/spaces/server/lib/request_inteceptors/on_post_auth_interceptor.ts index baf1bdc061c1..c5aaded074c3 100644 --- a/x-pack/plugins/spaces/server/lib/space_request_interceptors.ts +++ b/x-pack/plugins/spaces/server/lib/request_inteceptors/on_post_auth_interceptor.ts @@ -3,42 +3,23 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import Boom from 'boom'; +import { Server } from 'hapi'; +import { Space } from '../../../common/model/space'; +import { wrapError } from '../errors'; +import { getSpaceSelectorUrl } from '../get_space_selector_url'; +import { SpacesClient } from '../spaces_client'; +import { addSpaceIdToPath, getSpaceIdFromPath } from '../spaces_url_parser'; + +interface KbnServer extends Server { + getHiddenUiAppById: (appId: string) => any; +} -import { DEFAULT_SPACE_ID } from '../../common/constants'; -import { wrapError } from './errors'; -import { getSpaceSelectorUrl } from './get_space_selector_url'; -import { addSpaceIdToPath, getSpaceIdFromPath } from './spaces_url_parser'; - -export function initSpacesRequestInterceptors(server: any) { - const serverBasePath = server.config().get('server.basePath'); - - server.ext('onRequest', async function spacesOnRequestHandler(request: any, h: any) { - const path = request.path; - - // If navigating within the context of a space, then we store the Space's URL Context on the request, - // and rewrite the request to not include the space identifier in the URL. - const spaceId = getSpaceIdFromPath(path, serverBasePath); - - if (spaceId !== DEFAULT_SPACE_ID) { - const reqBasePath = `/s/${spaceId}`; - request.setBasePath(reqBasePath); - - const newLocation = path.substr(reqBasePath.length) || '/'; - - const newUrl = { - ...request.url, - path: newLocation, - pathname: newLocation, - href: newLocation, - }; - - request.setUrl(newUrl); - } - - return h.continue; - }); +export function initSpacesOnPostAuthRequestInterceptor(server: KbnServer) { + const serverBasePath: string = server.config().get('server.basePath'); + const xpackMainPlugin = server.plugins.xpack_main; - server.ext('onPostAuth', async function spacesOnRequestHandler(request: any, h: any) { + server.ext('onPostAuth', async function spacesOnPostAuthHandler(request: any, h: any) { const path = request.path; const isRequestingKibanaRoot = path === '/'; @@ -53,8 +34,8 @@ export function initSpacesRequestInterceptors(server: any) { const spaces = await spacesClient.getAll(); const config = server.config(); - const basePath = config.get('server.basePath'); - const defaultRoute = config.get('server.defaultRoute'); + const basePath: string = config.get('server.basePath'); + const defaultRoute: string = config.get('server.defaultRoute'); if (spaces.length === 1) { // If only one space is available, then send user there directly. @@ -78,14 +59,17 @@ export function initSpacesRequestInterceptors(server: any) { // This condition should only happen after selecting a space, or when transitioning from one application to another // e.g.: Navigating from Dashboard to Timelion if (isRequestingApplication) { - let spaceId; + let spaceId: string = ''; + let space: Space; try { - const spacesClient = server.plugins.spaces.spacesClient.getScopedClient(request); + const spacesClient: SpacesClient = server.plugins.spaces.spacesClient.getScopedClient( + request + ); spaceId = getSpaceIdFromPath(request.getBasePath(), serverBasePath); server.log(['spaces', 'debug'], `Verifying access to space "${spaceId}"`); - await spacesClient.get(spaceId); + space = await spacesClient.get(spaceId); } catch (error) { server.log( ['spaces', 'error'], @@ -94,6 +78,31 @@ export function initSpacesRequestInterceptors(server: any) { // Space doesn't exist, or user not authorized for space, or some other issue retrieving the active space. return h.redirect(getSpaceSelectorUrl(server.config())).takeover(); } + + // Verify application is available in this space + // The management page is always visible, so we shouldn't be restricting access to the kibana application in any situation. + const appId = path.split('/', 3)[2]; + if (appId !== 'kibana' && space && space.disabledFeatures.length > 0) { + server.log(['spaces', 'debug'], `Verifying application is available: "${appId}"`); + + const allFeatures = xpackMainPlugin.getFeatures(); + + const isRegisteredApp = allFeatures.some(feature => feature.app.includes(appId)); + if (isRegisteredApp) { + const enabledFeatures = allFeatures.filter( + feature => !space.disabledFeatures.includes(feature.id) + ); + + const isAvailableInSpace = enabledFeatures.some(feature => feature.app.includes(appId)); + if (!isAvailableInSpace) { + server.log( + ['spaces', 'error'], + `App ${appId} is not enabled within space "${spaceId}".` + ); + return Boom.notFound(); + } + } + } } return h.continue; }); diff --git a/x-pack/plugins/spaces/server/lib/request_inteceptors/on_request_interceptor.test.ts b/x-pack/plugins/spaces/server/lib/request_inteceptors/on_request_interceptor.test.ts new file mode 100644 index 000000000000..a8868f805049 --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/request_inteceptors/on_request_interceptor.test.ts @@ -0,0 +1,194 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Server } from 'hapi'; +import sinon from 'sinon'; + +import { initSpacesOnRequestInterceptor } from './on_request_interceptor'; + +describe('onRequestInterceptor', () => { + const sandbox = sinon.sandbox.create(); + const teardowns: Array<() => void> = []; + const headers = { + authorization: 'foo', + }; + let server: any; + let request: any; + + beforeEach(() => { + teardowns.push(() => sandbox.restore()); + request = async (path: string) => { + server = new Server(); + + interface Config { + [key: string]: any; + } + const config: Config = { + 'server.basePath': '/foo', + }; + + server.decorate( + 'server', + 'config', + jest.fn(() => { + return { + get: jest.fn(key => { + return config[key]; + }), + }; + }) + ); + + server.savedObjects = { + SavedObjectsClient: { + errors: { + isNotFoundError: (e: Error) => e.message === 'space not found', + }, + }, + getSavedObjectsRepository: jest.fn().mockImplementation(() => { + return { + get: (type: string, id: string) => { + if (type === 'space') { + if (id === 'not-found') { + throw new Error('space not found'); + } + return { + id, + name: 'test space', + }; + } + }, + create: () => null, + }; + }), + }; + + server.plugins = { + spaces: { + spacesClient: { + getScopedClient: jest.fn(), + }, + }, + }; + + initSpacesOnRequestInterceptor(server); + + server.route([ + { + method: 'GET', + path: '/', + handler: (req: any) => { + return { path: req.path, url: req.url, basePath: req.getBasePath() }; + }, + }, + { + method: 'GET', + path: '/app/kibana', + handler: (req: any) => { + return { path: req.path, url: req.url, basePath: req.getBasePath() }; + }, + }, + { + method: 'GET', + path: '/some/path/s/foo/bar', + handler: (req: any) => { + return { path: req.path, url: req.url, basePath: req.getBasePath() }; + }, + }, + { + method: 'GET', + path: '/i/love/spaces', + handler: (req: any) => { + return { path: req.path, query: req.query, url: req.url, basePath: req.getBasePath() }; + }, + }, + { + method: 'GET', + path: '/api/foo', + handler: (req: any) => { + return { path: req.path, url: req.url, basePath: req.getBasePath() }; + }, + }, + ]); + + let basePath: string | undefined; + server.decorate('request', 'getBasePath', () => basePath); + server.decorate('request', 'setBasePath', (newPath: string) => { + basePath = newPath; + }); + + teardowns.push(() => server.stop()); + + return await server.inject({ + method: 'GET', + url: path, + headers, + }); + }; + }); + + afterEach(async () => { + await Promise.all(teardowns.splice(0).map(fn => fn())); + }); + + describe('onRequest', () => { + test('handles paths without a space identifier', async () => { + const response = await request('/'); + + expect(response.statusCode).toEqual(200); + expect(JSON.parse(response.payload)).toMatchObject({ + path: '/', + url: { + path: '/', + pathname: '/', + href: '/', + }, + }); + }); + + test('strips the Space URL Context from the request', async () => { + const response = await request('/s/foo'); + expect(response.statusCode).toEqual(200); + expect(JSON.parse(response.payload)).toMatchObject({ + path: '/', + url: { + path: '/', + pathname: '/', + href: '/', + }, + }); + }); + + test('ignores space identifiers in the middle of the path', async () => { + const response = await request('/some/path/s/foo/bar'); + expect(response.statusCode).toEqual(200); + expect(JSON.parse(response.payload)).toMatchObject({ + path: '/some/path/s/foo/bar', + url: { + path: '/some/path/s/foo/bar', + pathname: '/some/path/s/foo/bar', + href: '/some/path/s/foo/bar', + }, + }); + }); + + test('strips the Space URL Context from the request, maintaining the rest of the path', async () => { + const response = await request('/s/foo/i/love/spaces?queryParam=queryValue'); + expect(response.statusCode).toEqual(200); + expect(JSON.parse(response.payload)).toMatchObject({ + path: '/i/love/spaces', + query: { + queryParam: 'queryValue', + }, + url: { + path: '/i/love/spaces', + pathname: '/i/love/spaces', + href: '/i/love/spaces', + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/spaces/server/lib/request_inteceptors/on_request_interceptor.ts b/x-pack/plugins/spaces/server/lib/request_inteceptors/on_request_interceptor.ts new file mode 100644 index 000000000000..9a5161eeab5f --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/request_inteceptors/on_request_interceptor.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Server } from 'hapi'; +import { DEFAULT_SPACE_ID } from '../../../common/constants'; +import { getSpaceIdFromPath } from '../spaces_url_parser'; + +export function initSpacesOnRequestInterceptor(server: Server) { + const serverBasePath: string = server.config().get('server.basePath'); + + server.ext('onRequest', async function spacesOnRequestHandler(request: any, h: any) { + const path = request.path; + + // If navigating within the context of a space, then we store the Space's URL Context on the request, + // and rewrite the request to not include the space identifier in the URL. + const spaceId = getSpaceIdFromPath(path, serverBasePath); + + if (spaceId !== DEFAULT_SPACE_ID) { + const reqBasePath = `/s/${spaceId}`; + request.setBasePath(reqBasePath); + + const newLocation = path.substr(reqBasePath.length) || '/'; + + const newUrl = { + ...request.url, + path: newLocation, + pathname: newLocation, + href: newLocation, + }; + + request.setUrl(newUrl); + } + + return h.continue; + }); +} diff --git a/x-pack/plugins/spaces/server/lib/space_request_interceptors.test.ts b/x-pack/plugins/spaces/server/lib/space_request_interceptors.test.ts deleted file mode 100644 index cef8fa788319..000000000000 --- a/x-pack/plugins/spaces/server/lib/space_request_interceptors.test.ts +++ /dev/null @@ -1,440 +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 { Server } from 'hapi'; -import sinon from 'sinon'; - -import { SavedObject } from 'src/legacy/server/saved_objects'; -import { initSpacesRequestInterceptors } from './space_request_interceptors'; - -describe('interceptors', () => { - const sandbox = sinon.sandbox.create(); - const teardowns: Array<() => void> = []; - const headers = { - authorization: 'foo', - }; - let server: any; - let request: any; - - beforeEach(() => { - teardowns.push(() => sandbox.restore()); - request = async ( - path: string, - setupFn: (ser: any) => void = () => { - return; - }, - testConfig = {} - ) => { - server = new Server(); - - interface Config { - [key: string]: any; - } - const config: Config = { - 'server.basePath': '/foo', - ...testConfig, - }; - - server.decorate( - 'server', - 'config', - jest.fn(() => { - return { - get: jest.fn(key => { - return config[key]; - }), - }; - }) - ); - - server.savedObjects = { - SavedObjectsClient: { - errors: { - isNotFoundError: (e: Error) => e.message === 'space not found', - }, - }, - getSavedObjectsRepository: jest.fn().mockImplementation(() => { - return { - get: (type: string, id: string) => { - if (type === 'space') { - if (id === 'not-found') { - throw new Error('space not found'); - } - return { - id, - name: 'test space', - }; - } - }, - create: () => null, - }; - }), - }; - - server.plugins = { - spaces: { - spacesClient: { - getScopedClient: jest.fn(), - }, - }, - }; - - initSpacesRequestInterceptors(server); - - server.route([ - { - method: 'GET', - path: '/', - handler: (req: any) => { - return { path: req.path, url: req.url, basePath: req.getBasePath() }; - }, - }, - { - method: 'GET', - path: '/app/kibana', - handler: (req: any) => { - return { path: req.path, url: req.url, basePath: req.getBasePath() }; - }, - }, - { - method: 'GET', - path: '/api/foo', - handler: (req: any) => { - return { path: req.path, url: req.url, basePath: req.getBasePath() }; - }, - }, - ]); - - await setupFn(server); - - let basePath: string | undefined; - server.decorate('request', 'getBasePath', () => basePath); - server.decorate('request', 'setBasePath', (newPath: string) => { - basePath = newPath; - }); - - teardowns.push(() => server.stop()); - - return await server.inject({ - method: 'GET', - url: path, - headers, - }); - }; - }); - - afterEach(async () => { - await Promise.all(teardowns.splice(0).map(fn => fn())); - }); - - describe('onRequest', () => { - test('handles paths without a space identifier', async () => { - const testHandler = jest.fn((req, h) => { - expect(req.path).toBe('/'); - return h.continue; - }); - - await request('/', (hapiServer: any) => { - hapiServer.ext('onRequest', testHandler); - }); - - expect(testHandler).toHaveBeenCalledTimes(1); - }); - - test('strips the Space URL Context from the request', async () => { - const testHandler = jest.fn((req, h) => { - expect(req.path).toBe('/'); - return h.continue; - }); - - await request('/s/foo', (hapiServer: any) => { - hapiServer.ext('onRequest', testHandler); - }); - - expect(testHandler).toHaveBeenCalledTimes(1); - }); - - test('ignores space identifiers in the middle of the path', async () => { - const testHandler = jest.fn((req, h) => { - expect(req.path).toBe('/some/path/s/foo/bar'); - return h.continue; - }); - - await request('/some/path/s/foo/bar', (hapiServer: any) => { - hapiServer.ext('onRequest', testHandler); - }); - - expect(testHandler).toHaveBeenCalledTimes(1); - }); - - test('strips the Space URL Context from the request, maintaining the rest of the path', async () => { - const testHandler = jest.fn((req, h) => { - expect(req.path).toBe('/i/love/spaces.html'); - expect(req.query).toEqual({ - queryParam: 'queryValue', - }); - return h.continue; - }); - - await request('/s/foo/i/love/spaces.html?queryParam=queryValue', (hapiServer: any) => { - hapiServer.ext('onRequest', testHandler); - }); - - expect(testHandler).toHaveBeenCalledTimes(1); - }); - }); - - describe('onPostAuth', () => { - const serverBasePath = '/my/base/path'; - const defaultRoute = '/app/custom-app'; - - const config = { - 'server.basePath': serverBasePath, - 'server.defaultRoute': defaultRoute, - }; - - const setupTest = (hapiServer: any, spaces: SavedObject[], testHandler: any) => { - hapiServer.plugins.spaces.spacesClient.getScopedClient.mockReturnValue({ - getAll() { - return spaces; - }, - }); - - // Register test inspector - hapiServer.ext('onPreResponse', testHandler); - }; - - describe('when accessing an app within a non-existent space', () => { - it('redirects to the space selector screen', async () => { - const testHandler = jest.fn((req, h) => { - const { response } = req; - - if (response && response.isBoom) { - throw response; - } - - expect(response.statusCode).toEqual(302); - expect(response.headers.location).toEqual(serverBasePath); - - return h.continue; - }); - - const spaces = [ - { - id: 'a-space', - type: 'space', - attributes: { - name: 'a space', - }, - references: [], - }, - ]; - - await request( - '/s/not-found/app/kibana', - (hapiServer: any) => { - setupTest(hapiServer, spaces, testHandler); - }, - config - ); - - expect(testHandler).toHaveBeenCalledTimes(1); - }); - }); - - describe('when accessing an API endpoint within a non-existent space', () => { - it('allows the request to continue', async () => { - const testHandler = jest.fn((req, h) => { - const { response } = req; - - if (response && response.isBoom) { - throw response; - } - - expect(response.statusCode).toEqual(200); - - return h.continue; - }); - - const spaces = [ - { - id: 'a-space', - type: 'space', - attributes: { - name: 'a space', - }, - references: [], - }, - ]; - - await request( - '/s/not-found/api/foo', - (hapiServer: any) => { - setupTest(hapiServer, spaces, testHandler); - }, - config - ); - - expect(testHandler).toHaveBeenCalledTimes(1); - }); - }); - - describe('with a single available space', () => { - test('it redirects to the defaultRoute within the context of the single Space when navigating to Kibana root', async () => { - const testHandler = jest.fn((req, h) => { - const { response } = req; - - if (response && response.isBoom) { - throw response; - } - - expect(response.statusCode).toEqual(302); - expect(response.headers.location).toEqual(`${serverBasePath}/s/a-space${defaultRoute}`); - - return h.continue; - }); - - const spaces = [ - { - id: 'a-space', - type: 'space', - attributes: { - name: 'a space', - }, - references: [], - }, - ]; - - await request( - '/', - (hapiServer: any) => { - setupTest(server, spaces, testHandler); - }, - config - ); - - expect(testHandler).toHaveBeenCalledTimes(1); - expect(server.plugins.spaces.spacesClient.getScopedClient).toHaveBeenCalledWith( - expect.objectContaining({ - headers: expect.objectContaining({ - authorization: headers.authorization, - }), - }) - ); - }); - - test('it redirects to the defaultRoute within the context of the Default Space when navigating to Kibana root', async () => { - // This is very similar to the test above, but this handles the condition where the only available space is the Default Space, - // which does not have a URL Context. In this scenario, the end result is the same as the other test, but the final URL the user - // is redirected to does not contain a space identifier (e.g., /s/foo) - - const testHandler = jest.fn((req, h) => { - const { response } = req; - - if (response && response.isBoom) { - throw response; - } - - expect(response.statusCode).toEqual(302); - expect(response.headers.location).toEqual(`${serverBasePath}${defaultRoute}`); - - return h.continue; - }); - - const spaces = [ - { - id: 'default', - type: 'space', - attributes: { - name: 'Default Space', - }, - references: [], - }, - ]; - - await request( - '/', - (hapiServer: any) => { - setupTest(hapiServer, spaces, testHandler); - }, - config - ); - - expect(testHandler).toHaveBeenCalledTimes(1); - expect(server.plugins.spaces.spacesClient.getScopedClient).toHaveBeenCalledWith( - expect.objectContaining({ - headers: expect.objectContaining({ - authorization: headers.authorization, - }), - }) - ); - }); - }); - - describe('with multiple available spaces', () => { - test('it redirects to the Space Selector App when navigating to Kibana root', async () => { - const spaces = [ - { - id: 'a-space', - type: 'space', - attributes: { - name: 'a space', - }, - references: [], - }, - { - id: 'b-space', - type: 'space', - attributes: { - name: 'b space', - }, - references: [], - }, - ]; - - const getHiddenUiAppHandler = jest.fn(() => '
space selector
'); - - const testHandler = jest.fn((req, h) => { - const { response } = req; - - if (response && response.isBoom) { - throw response; - } - - expect(response.statusCode).toEqual(200); - expect(response.source).toEqual({ app: '
space selector
', renderApp: true }); - - return h.continue; - }); - - await request( - '/', - (hapiServer: any) => { - server.decorate('server', 'getHiddenUiAppById', getHiddenUiAppHandler); - server.decorate('toolkit', 'renderApp', function renderAppHandler(app: any) { - // @ts-ignore - this({ renderApp: true, app }); - }); - - setupTest(hapiServer, spaces, testHandler); - }, - config - ); - - expect(getHiddenUiAppHandler).toHaveBeenCalledTimes(1); - expect(getHiddenUiAppHandler).toHaveBeenCalledWith('space_selector'); - expect(testHandler).toHaveBeenCalledTimes(1); - expect(server.plugins.spaces.spacesClient.getScopedClient).toHaveBeenCalledWith( - expect.objectContaining({ - headers: expect.objectContaining({ - authorization: headers.authorization, - }), - }) - ); - }); - }); - }); -}); diff --git a/x-pack/plugins/spaces/server/lib/space_schema.ts b/x-pack/plugins/spaces/server/lib/space_schema.ts index d27a855e0f91..0ffaf9ce51cf 100644 --- a/x-pack/plugins/spaces/server/lib/space_schema.ts +++ b/x-pack/plugins/spaces/server/lib/space_schema.ts @@ -13,5 +13,6 @@ export const spaceSchema = Joi.object({ description: Joi.string().allow(''), initials: Joi.string().max(MAX_SPACE_INITIALS), color: Joi.string().regex(/^#[a-z0-9]{6}$/, `6 digit hex color, starting with a #`), + disabledFeatures: Joi.array().items(Joi.string()), _reserved: Joi.boolean(), }).default(); diff --git a/x-pack/plugins/spaces/server/lib/spaces_client.test.ts b/x-pack/plugins/spaces/server/lib/spaces_client.test.ts index 360a51ecdd79..92213bd5dce1 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_client.test.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_client.test.ts @@ -25,7 +25,9 @@ const createMockAuthorization = () => { const mockAuthorization = { actions: { login: 'action:login', - manageSpaces: 'action:manageSpaces', + space: { + manage: 'space:manage', + }, }, checkPrivilegesWithRequest: jest.fn(() => ({ atSpaces: mockCheckPrivilegesAtSpaces, @@ -367,7 +369,7 @@ describe('#canEnumerateSpaces', () => { expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( - mockAuthorization.actions.manageSpaces + mockAuthorization.actions.space.manage ); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); @@ -401,7 +403,7 @@ describe('#canEnumerateSpaces', () => { expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( - mockAuthorization.actions.manageSpaces + mockAuthorization.actions.space.manage ); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); @@ -570,12 +572,14 @@ describe('#create', () => { description: 'foo-description', bar: 'foo-bar', _reserved: true, + disabledFeatures: [], }; const attributes = { name: 'foo-name', description: 'foo-description', bar: 'foo-bar', + disabledFeatures: [], }; const savedObject = { @@ -584,6 +588,7 @@ describe('#create', () => { name: 'foo-name', description: 'foo-description', bar: 'foo-bar', + disabledFeatures: [], }, }; @@ -592,6 +597,7 @@ describe('#create', () => { name: 'foo-name', description: 'foo-description', bar: 'foo-bar', + disabledFeatures: [], }; describe(`authorization is null`, () => { @@ -788,7 +794,7 @@ describe('#create', () => { expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( - mockAuthorization.actions.manageSpaces + mockAuthorization.actions.space.manage ); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledWith(username, 'create'); expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(0); @@ -840,7 +846,7 @@ describe('#create', () => { expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( - mockAuthorization.actions.manageSpaces + mockAuthorization.actions.space.manage ); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledWith(username, 'create'); @@ -889,7 +895,7 @@ describe('#create', () => { expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( - mockAuthorization.actions.manageSpaces + mockAuthorization.actions.space.manage ); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledWith(username, 'create'); @@ -904,12 +910,14 @@ describe('#update', () => { description: 'foo-description', bar: 'foo-bar', _reserved: false, + disabledFeatures: [], }; const attributes = { name: 'foo-name', description: 'foo-description', bar: 'foo-bar', + disabledFeatures: [], }; const savedObject = { @@ -919,6 +927,7 @@ describe('#update', () => { description: 'foo-description', bar: 'foo-bar', _reserved: true, + disabledFeatures: [], }, }; @@ -928,6 +937,7 @@ describe('#update', () => { description: 'foo-description', bar: 'foo-bar', _reserved: true, + disabledFeatures: [], }; describe(`authorization is null`, () => { @@ -1021,7 +1031,7 @@ describe('#update', () => { expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( - mockAuthorization.actions.manageSpaces + mockAuthorization.actions.space.manage ); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledWith(username, 'update'); expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(0); @@ -1059,7 +1069,7 @@ describe('#update', () => { expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( - mockAuthorization.actions.manageSpaces + mockAuthorization.actions.space.manage ); expect(mockInternalRepository.update).toHaveBeenCalledWith('space', id, attributes); expect(mockInternalRepository.get).toHaveBeenCalledWith('space', id); @@ -1240,7 +1250,7 @@ describe('#delete', () => { expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( - mockAuthorization.actions.manageSpaces + mockAuthorization.actions.space.manage ); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledWith(username, 'delete'); expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(0); @@ -1275,7 +1285,7 @@ describe('#delete', () => { expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( - mockAuthorization.actions.manageSpaces + mockAuthorization.actions.space.manage ); expect(mockInternalRepository.get).toHaveBeenCalledWith('space', id); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); @@ -1314,7 +1324,7 @@ describe('#delete', () => { expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( - mockAuthorization.actions.manageSpaces + mockAuthorization.actions.space.manage ); expect(mockInternalRepository.get).toHaveBeenCalledWith('space', id); expect(mockInternalRepository.delete).toHaveBeenCalledWith('space', id); diff --git a/x-pack/plugins/spaces/server/lib/spaces_client.ts b/x-pack/plugins/spaces/server/lib/spaces_client.ts index 52085cf6b2e5..5d14cb99447c 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_client.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_client.ts @@ -24,7 +24,7 @@ export class SpacesClient { if (this.useRbac()) { const checkPrivileges = this.authorization.checkPrivilegesWithRequest(this.request); const { hasAllRequested } = await checkPrivileges.globally( - this.authorization.actions.manageSpaces + this.authorization.actions.space.manage ); this.debugLogger(`SpacesClient.canEnumerateSpaces, using RBAC. Result: ${hasAllRequested}`); return hasAllRequested; @@ -121,7 +121,7 @@ export class SpacesClient { this.debugLogger(`SpacesClient.create(), using RBAC. Checking if authorized globally`); await this.ensureAuthorizedGlobally( - this.authorization.actions.manageSpaces, + this.authorization.actions.space.manage, 'create', 'Unauthorized to create spaces' ); @@ -157,7 +157,7 @@ export class SpacesClient { public async update(id: string, space: Space) { if (this.useRbac()) { await this.ensureAuthorizedGlobally( - this.authorization.actions.manageSpaces, + this.authorization.actions.space.manage, 'update', 'Unauthorized to update spaces' ); @@ -175,7 +175,7 @@ export class SpacesClient { public async delete(id: string) { if (this.useRbac()) { await this.ensureAuthorizedGlobally( - this.authorization.actions.manageSpaces, + this.authorization.actions.space.manage, 'delete', 'Unauthorized to delete spaces' ); diff --git a/x-pack/plugins/spaces/server/lib/toggle_ui_capabilities.test.ts b/x-pack/plugins/spaces/server/lib/toggle_ui_capabilities.test.ts new file mode 100644 index 000000000000..fb9c8a85a4e2 --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/toggle_ui_capabilities.test.ts @@ -0,0 +1,169 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UICapabilities } from 'ui/capabilities'; +import { Feature } from '../../../xpack_main/types'; +import { Space } from '../../common/model/space'; +import { toggleUICapabilities } from './toggle_ui_capabilities'; + +const features: Feature[] = [ + { + id: 'feature_1', + name: 'Feature 1', + app: [], + privileges: {}, + }, + { + id: 'feature_2', + name: 'Feature 2', + navLinkId: 'feature2', + app: [], + catalogue: ['feature2Entry'], + management: { + kibana: ['somethingElse'], + }, + privileges: { + all: { + app: [], + ui: [], + savedObject: { + all: [], + read: [], + }, + }, + }, + }, + { + id: 'feature_3', + name: 'Feature 3', + navLinkId: 'feature3', + app: [], + catalogue: ['feature3Entry'], + management: { + kibana: ['indices'], + }, + privileges: { + all: { + app: [], + ui: [], + savedObject: { + all: [], + read: [], + }, + }, + }, + }, +]; + +const buildUiCapabilities = () => + Object.freeze({ + navLinks: { + feature1: true, + feature2: true, + feature3: true, + unknownFeature: true, + }, + catalogue: { + discover: true, + visualize: false, + }, + management: { + kibana: { + settings: false, + indices: true, + somethingElse: true, + }, + }, + feature_1: { + foo: true, + bar: true, + }, + feature_2: { + foo: true, + bar: true, + }, + feature_3: { + foo: true, + bar: true, + }, + }) as UICapabilities; + +describe('toggleUiCapabilities', () => { + it('does not toggle capabilities when the space has no disabled features', () => { + const space: Space = { + id: 'space', + name: '', + disabledFeatures: [], + }; + + const uiCapabilities: UICapabilities = buildUiCapabilities(); + const result = toggleUICapabilities(features, uiCapabilities, space); + expect(result).toEqual(buildUiCapabilities()); + }); + + it('ignores unknown disabledFeatures', () => { + const space: Space = { + id: 'space', + name: '', + disabledFeatures: ['i-do-not-exist'], + }; + + const uiCapabilities: UICapabilities = buildUiCapabilities(); + const result = toggleUICapabilities(features, uiCapabilities, space); + expect(result).toEqual(buildUiCapabilities()); + }); + + it('disables the corresponding navLink, catalogue, management sections, and all capability flags for disabled features', () => { + const space: Space = { + id: 'space', + name: '', + disabledFeatures: ['feature_2'], + }; + + const uiCapabilities: UICapabilities = buildUiCapabilities(); + const result = toggleUICapabilities(features, uiCapabilities, space); + + const expectedCapabilities = buildUiCapabilities(); + + expectedCapabilities.navLinks.feature2 = false; + expectedCapabilities.catalogue.feature2Entry = false; + expectedCapabilities.management.kibana.somethingElse = false; + expectedCapabilities.feature_2.bar = false; + expectedCapabilities.feature_2.foo = false; + + expect(result).toEqual(expectedCapabilities); + }); + + it('can disable everything', () => { + const space: Space = { + id: 'space', + name: '', + disabledFeatures: ['feature_1', 'feature_2', 'feature_3'], + }; + + const uiCapabilities: UICapabilities = buildUiCapabilities(); + const result = toggleUICapabilities(features, uiCapabilities, space); + + const expectedCapabilities = buildUiCapabilities(); + + expectedCapabilities.feature_1.bar = false; + expectedCapabilities.feature_1.foo = false; + + expectedCapabilities.navLinks.feature2 = false; + expectedCapabilities.catalogue.feature2Entry = false; + expectedCapabilities.management.kibana.somethingElse = false; + expectedCapabilities.feature_2.bar = false; + expectedCapabilities.feature_2.foo = false; + + expectedCapabilities.navLinks.feature3 = false; + expectedCapabilities.catalogue.feature3Entry = false; + expectedCapabilities.management.kibana.indices = false; + expectedCapabilities.feature_3.bar = false; + expectedCapabilities.feature_3.foo = false; + + expect(result).toEqual(expectedCapabilities); + }); +}); diff --git a/x-pack/plugins/spaces/server/lib/toggle_ui_capabilities.ts b/x-pack/plugins/spaces/server/lib/toggle_ui_capabilities.ts new file mode 100644 index 000000000000..4cec018959ab --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/toggle_ui_capabilities.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import _ from 'lodash'; +import { UICapabilities } from 'ui/capabilities'; +import { Feature } from '../../../xpack_main/types'; +import { Space } from '../../common/model/space'; + +export function toggleUICapabilities( + features: Feature[], + uiCapabilities: UICapabilities, + activeSpace: Space +) { + const clonedCapabilities = _.cloneDeep(uiCapabilities); + + toggleDisabledFeatures(features, clonedCapabilities, activeSpace); + + return clonedCapabilities; +} + +function toggleDisabledFeatures( + features: Feature[], + uiCapabilities: UICapabilities, + activeSpace: Space +) { + const disabledFeatureKeys: string[] = activeSpace.disabledFeatures; + + const disabledFeatures: Feature[] = disabledFeatureKeys + .map(key => features.find(feature => feature.id === key)) + .filter(feature => typeof feature !== 'undefined') as Feature[]; + + const navLinks: Record = uiCapabilities.navLinks; + const catalogueEntries: Record = uiCapabilities.catalogue; + const managementItems: Record> = uiCapabilities.management; + + for (const feature of disabledFeatures) { + // Disable associated navLink, if one exists + if (feature.navLinkId && navLinks.hasOwnProperty(feature.navLinkId)) { + navLinks[feature.navLinkId] = false; + } + + // Disable associated catalogue entries + const privilegeCatalogueEntries: string[] = feature.catalogue || []; + privilegeCatalogueEntries.forEach(catalogueEntryId => { + catalogueEntries[catalogueEntryId] = false; + }); + + // Disable associated management items + const privilegeManagementSections: Record = feature.management || {}; + Object.entries(privilegeManagementSections).forEach(([sectionId, sectionItems]) => { + sectionItems.forEach(item => { + if ( + managementItems.hasOwnProperty(sectionId) && + managementItems[sectionId].hasOwnProperty(item) + ) { + managementItems[sectionId][item] = false; + } + }); + }); + + // Disable "sub features" that match the disabled feature + if (uiCapabilities.hasOwnProperty(feature.id)) { + const capability = uiCapabilities[feature.id]; + Object.keys(capability).forEach(featureKey => { + capability[featureKey] = false; + }); + } + } +} diff --git a/x-pack/plugins/security/public/views/management/edit_role/lib/copy_role.ts b/x-pack/plugins/spaces/types.d.ts similarity index 60% rename from x-pack/plugins/security/public/views/management/edit_role/lib/copy_role.ts rename to x-pack/plugins/spaces/types.d.ts index 395f14756c54..98a20203c13c 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/lib/copy_role.ts +++ b/x-pack/plugins/spaces/types.d.ts @@ -3,10 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { Request } from 'hapi'; -import { cloneDeep } from 'lodash'; -import { Role } from '../../../../../common/model/role'; - -export function copyRole(role: Role) { - return cloneDeep(role); +export interface SpacesPlugin { + getSpaceId(request: Record): string; } diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 706ac4a51e2d..5e59b9b69c42 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -7457,75 +7457,41 @@ "xpack.security.management.editRole.setPrivilegesToKibanaSpacesDescription": "设置 Elasticsearch 数据的权限并控制对 Kibana 空间的访问权限。", "xpack.security.management.editRole.updateRoleText": "更新角色", "xpack.security.management.editRole.viewingRoleTitle": "正在查看角色", - "xpack.security.management.editRoles.deleteRoleButton.cancelButtonLabel": "不,不删除", - "xpack.security.management.editRoles.deleteRoleButton.confirmButtonLabel": "是的,删除角色", - "xpack.security.management.editRoles.deleteRoleButton.deleteRoleButtonLabel": "删除角色", - "xpack.security.management.editRoles.deleteRoleButton.deleteRoleTitle": "删除角色", - "xpack.security.management.editRoles.deleteRoleButton.deletingRoleConfirmationText": "是否确定要删除此角色?", - "xpack.security.management.editRoles.deleteRoleButton.deletingRoleWarningText": "此操作无法撤消!", - "xpack.security.management.editRoles.elasticSearchPrivileges.addIndexPrivilegesButtonLabel": "添加索引权限", - "xpack.security.management.editRoles.elasticSearchPrivileges.addUserTitle": "添加用户……", - "xpack.security.management.editRoles.elasticSearchPrivileges.clusterPrivilegesTitle": "集群权限", - "xpack.security.management.editRoles.elasticSearchPrivileges.controlAccessToClusterDataDescription": "控制对集群中数据的访问权限。", - "xpack.security.management.editRoles.elasticSearchPrivileges.howToBeSubmittedOnBehalfOfOtherUsersDescription": "允许代表其他用户提交请求。", - "xpack.security.management.editRoles.elasticSearchPrivileges.indexPrivilegesTitle": "索引权限", - "xpack.security.management.editRoles.elasticSearchPrivileges.learnMoreLinkText": "了解详情", - "xpack.security.management.editRoles.elasticSearchPrivileges.manageRoleActionsDescription": "管理此角色可以对您的集群执行的操作。", - "xpack.security.management.editRoles.elasticSearchPrivileges.runAsPrivilegesTitle": "以权限角色运行", - "xpack.security.management.editRoles.impactedSpacesFlyout.allLabel": "全部", - "xpack.security.management.editRoles.impactedSpacesFlyout.noneLabel": "无", - "xpack.security.management.editRoles.impactedSpacesFlyout.readLabel": "读取", - "xpack.security.management.editRoles.impactedSpacesFlyout.spacePrivilegesSummaryTitle": "工作区权限摘要", - "xpack.security.management.editRoles.impactedSpacesFlyout.viewSpacesPrivilegesSummaryLinkText": "查看工作区权限摘要", - "xpack.security.management.editRoles.indexPrivilegeForm.deleteSpacePrivilegeAriaLabel": "删除索引权限", - "xpack.security.management.editRoles.indexPrivilegeForm.grantedDocumentsQueryFormRowLabel": "授权的文档查询", - "xpack.security.management.editRoles.indexPrivilegeForm.grantedFieldsFormRowHelpText": "如果未授权任何字段,则分配到此角色的用户将无法查看此索引的任何数据。", - "xpack.security.management.editRoles.indexPrivilegeForm.grantedFieldsFormRowLabel": "授权字段(可选)", - "xpack.security.management.editRoles.indexPrivilegeForm.grantReadPrivilegesLabel": "向特定文档授予读取权限", - "xpack.security.management.editRoles.indexPrivilegeForm.indicesFormRowLabel": "索引", - "xpack.security.management.editRoles.indexPrivilegeForm.privilegesFormRowLabel": "权限", - "xpack.security.management.editRoles.privilegeCalloutWarning.allText": "全部", - "xpack.security.management.editRoles.privilegeCalloutWarning.alwaysGrantReadAccessToAllSpacesTitle": "此角色始终授予对所有空间的读取权限。要定制各个空间的权限,必须创建一个新角色。", - "xpack.security.management.editRoles.privilegeCalloutWarning.howToCustomizePrivilegesDescription": "此角色始终授予对所有空间的完全访问权限。要定制各个空间的权限,必须创建一个新角色。", - "xpack.security.management.editRoles.privilegeCalloutWarning.howToCustomizePrivilegesForIndividualSpacesDescription": "将最低权限设置为 {allText} 可以授予对所有空间的完全访问权限。要定制各个空间的权限,最低权限必须为 {readText} 或 {noneText}。", - "xpack.security.management.editRoles.privilegeCalloutWarning.minimalPossiblePrivilageTitle": "可能的最低权限是 {readText}。", - "xpack.security.management.editRoles.privilegeCalloutWarning.minimumPrivilegeTitle": "最低权限太高,无法定制各个空间", - "xpack.security.management.editRoles.privilegeCalloutWarning.neverGrantReadAccessToAllSpacesTitle": "此角色永远不会授予对 Kibana 内任何空间的访问权限。要定制各个空间的权限,必须创建一个新角色。", - "xpack.security.management.editRoles.privilegeCalloutWarning.noneText": "无", - "xpack.security.management.editRoles.privilegeCalloutWarning.notPossibleToCustomizeReservedRoleSpacePrivilegesTitle": "无法定制保留角色的空间权限", - "xpack.security.management.editRoles.privilegeCalloutWarning.readText": "读取", - "xpack.security.management.editRoles.privilegeSpaceForm.deleteSpacePrivilegeAriaLabel": "删除空间权限", - "xpack.security.management.editRoles.privilegeSpaceForm.privilegeFormRowLabel": "权限", - "xpack.security.management.editRoles.privilegeSpaceForm.spacesFormRowLabel": "工作区", - "xpack.security.management.editRoles.privilegeSpaceTable.actionsName": "操作", - "xpack.security.management.editRoles.privilegeSpaceTable.deletedSpaceDescription": "{value}(已删除)", - "xpack.security.management.editRoles.privilegeSpaceTable.filterPlaceholder": "筛选", - "xpack.security.management.editRoles.privilegeSpaceTable.privilegeName": "权限", - "xpack.security.management.editRoles.privilegeSpaceTable.spaceName": "工作区", - "xpack.security.management.editRoles.reversedRoleBadget.reversedRolesCanNotBeRemovedTooltip": "保留的角色是内置的,无法删除或修改。", - "xpack.security.management.editRoles.simplePrivilegeForm.kibanaPrivilegesTitle": "Kibana 权限", - "xpack.security.management.editRoles.simplePrivilegeForm.specifyPrivilegeForRoleDescription": "为此角色指定 Kibana 权限。", - "xpack.security.management.editRoles.spaceAwarePrivilegeForm.addSpacePrivilegeTitle": "添加工作区权限", - "xpack.security.management.editRoles.spaceAwarePrivilegeForm.allText": "全部", - "xpack.security.management.editRoles.spaceAwarePrivilegeForm.ensureAccountHasAllPrivilegesGrantedDescription": "请确保您的帐户具有 {kibanaUser} 角色授予的所有权限,然后重试。", - "xpack.security.management.editRoles.spaceAwarePrivilegeForm.grantMorePrivilegesTitle": "基于每个工作区授予更多权限。例如,如果所有工作区的权限均为 {read},则可以将单个工作区的权限设置为 {all}。", - "xpack.security.management.editRoles.spaceAwarePrivilegeForm.higherPrivilegesForIndividualSpacesTitle": "针对各个工作区的更高权限", - "xpack.security.management.editRoles.spaceAwarePrivilegeForm.howToViewAllAvailableSpacesDescription": "您没有权限查看所有可用工作区。", - "xpack.security.management.editRoles.spaceAwarePrivilegeForm.insufficientPrivilegesDescription": "权限不足", - "xpack.security.management.editRoles.spaceAwarePrivilegeForm.kibanaUserTitle": "kibana_user", - "xpack.security.management.editRoles.spaceAwarePrivilegeForm.minimumActionsUserCanPerformInYourSpacesDescription": "指定用户可在您的工作区中执行的最少操作。", - "xpack.security.management.editRoles.spaceAwarePrivilegeForm.minPrivilegesForAllSpacesTitle": "针对所有工作区的最低权限", - "xpack.security.management.editRoles.spaceAwarePrivilegeForm.noAccessToSpacesHelpText": "无工作区访问权限", - "xpack.security.management.editRoles.spaceAwarePrivilegeForm.readText": "读取", - "xpack.security.management.editRoles.spaceAwarePrivilegeForm.viewEditShareAppsWithinAllSpacesHelpText": "在所有工作区内查看、编辑和共享对象及应用", - "xpack.security.management.editRoles.spaceAwarePrivilegeForm.viewObjectsAndAppsWithinAllSpacesHelpText": "在所有工作区内查看对象和应用", - "xpack.security.management.editRoles.validateRole.indicesTypeErrorMessage": "{elasticIndices} 应为一个数组", - "xpack.security.management.editRoles.validateRole.nameAllowedCharactersWarningMessage": "名称必须以字母或下划线开头,且只能包含字母、下划线和数字。", - "xpack.security.management.editRoles.validateRole.nameLengthWarningMessage": "名称不能超过 1024 个字符", - "xpack.security.management.editRoles.validateRole.onePrivilegeRequiredWarningMessage": "至少需要一个权限", - "xpack.security.management.editRoles.validateRole.oneSpaceRequiredWarningMessage": "至少需要一个工作区", - "xpack.security.management.editRoles.validateRole.privilegeRequiredWarningMessage": "“权限”必填", - "xpack.security.management.editRoles.validateRole.provideRoleNameWarningMessage": "请提供一个角色名称", + "xpack.security.management.editRole.deleteRoleButton.cancelButtonLabel": "不,不删除", + "xpack.security.management.editRole.deleteRoleButton.confirmButtonLabel": "是的,删除角色", + "xpack.security.management.editRole.deleteRoleButton.deleteRoleButtonLabel": "删除角色", + "xpack.security.management.editRole.deleteRoleButton.deleteRoleTitle": "删除角色", + "xpack.security.management.editRole.deleteRoleButton.deletingRoleConfirmationText": "是否确定要删除此角色?", + "xpack.security.management.editRole.deleteRoleButton.deletingRoleWarningText": "此操作无法撤消!", + "xpack.security.management.editRole.elasticSearchPrivileges.addIndexPrivilegesButtonLabel": "添加索引权限", + "xpack.security.management.editRole.elasticSearchPrivileges.addUserTitle": "添加用户……", + "xpack.security.management.editRole.elasticSearchPrivileges.clusterPrivilegesTitle": "集群权限", + "xpack.security.management.editRole.elasticSearchPrivileges.controlAccessToClusterDataDescription": "控制对集群中数据的访问权限。", + "xpack.security.management.editRole.elasticSearchPrivileges.howToBeSubmittedOnBehalfOfOtherUsersDescription": "允许代表其他用户提交请求。", + "xpack.security.management.editRole.elasticSearchPrivileges.indexPrivilegesTitle": "索引权限", + "xpack.security.management.editRole.elasticSearchPrivileges.learnMoreLinkText": "了解详情", + "xpack.security.management.editRole.elasticSearchPrivileges.manageRoleActionsDescription": "管理此角色可以对您的集群执行的操作。", + "xpack.security.management.editRole.elasticSearchPrivileges.runAsPrivilegesTitle": "以权限角色运行", + "xpack.security.management.editRole.indexPrivilegeForm.deleteSpacePrivilegeAriaLabel": "删除索引权限", + "xpack.security.management.editRole.indexPrivilegeForm.grantedDocumentsQueryFormRowLabel": "授权的文档查询", + "xpack.security.management.editRole.indexPrivilegeForm.grantedFieldsFormRowHelpText": "如果未授权任何字段,则分配到此角色的用户将无法查看此索引的任何数据。", + "xpack.security.management.editRole.indexPrivilegeForm.grantedFieldsFormRowLabel": "授权字段(可选)", + "xpack.security.management.editRole.indexPrivilegeForm.grantReadPrivilegesLabel": "向特定文档授予读取权限", + "xpack.security.management.editRole.indexPrivilegeForm.indicesFormRowLabel": "索引", + "xpack.security.management.editRole.indexPrivilegeForm.privilegesFormRowLabel": "权限", + "xpack.security.management.editRole.simplePrivilegeForm.kibanaPrivilegesTitle": "Kibana 权限", + "xpack.security.management.editRole.simplePrivilegeForm.specifyPrivilegeForRoleDescription": "为此角色指定 Kibana 权限。", + "xpack.security.management.editRole.spaceAwarePrivilegeForm.ensureAccountHasAllPrivilegesGrantedDescription": "请确保您的帐户具有 {kibanaUser} 角色授予的所有权限,然后重试。", + "xpack.security.management.editRole.spaceAwarePrivilegeForm.howToViewAllAvailableSpacesDescription": "您没有权限查看所有可用工作区。", + "xpack.security.management.editRole.spaceAwarePrivilegeForm.insufficientPrivilegesDescription": "权限不足", + "xpack.security.management.editRole.spaceAwarePrivilegeForm.kibanaUserTitle": "kibana_user", + "xpack.security.management.editRole.validateRole.indicesTypeErrorMessage": "{elasticIndices} 应为一个数组", + "xpack.security.management.editRole.validateRole.nameAllowedCharactersWarningMessage": "名称必须以字母或下划线开头,且只能包含字母、下划线和数字。", + "xpack.security.management.editRole.validateRole.nameLengthWarningMessage": "名称不能超过 1024 个字符", + "xpack.security.management.editRole.validateRole.onePrivilegeRequiredWarningMessage": "至少需要一个权限", + "xpack.security.management.editRole.validateRole.oneSpaceRequiredWarningMessage": "至少需要一个工作区", + "xpack.security.management.editRole.validateRole.privilegeRequiredWarningMessage": "“权限”必填", + "xpack.security.management.editRole.validateRole.provideRoleNameWarningMessage": "请提供一个角色名称", "xpack.security.management.passwordForm.confirmPasswordLabel": "确认密码", "xpack.security.management.passwordForm.passwordDontMatchDescription": "密码不匹配", "xpack.security.management.passwordForm.passwordLabel": "密码", @@ -7624,33 +7590,20 @@ "xpack.spaces.management.confirmDeleteModal.redirectAfterDeletingCurrentSpaceWarningMessage": "您即将删除当前空间 {name}。如果继续,系统会将您重定向到选择其他空间的位置。", "xpack.spaces.management.confirmDeleteModal.spaceNamesDoNoMatchErrorMessage": "空间名称不匹配。", "xpack.spaces.management.customizeSpaceAvatar.colorFormRowLabel": "颜色", - "xpack.spaces.management.customizeSpaceAvatar.customizeLinkText": "定制", "xpack.spaces.management.customizeSpaceAvatar.initialItemsFormRowLabel": "名字缩写(最多两个字符)", "xpack.spaces.management.deleteSpacesButton.deleteSpaceAriaLabel": "删除此空间", "xpack.spaces.management.deleteSpacesButton.deleteSpaceButtonLabel": "删除空间", "xpack.spaces.management.deleteSpacesButton.deleteSpaceErrorTitle": "删除空间时出错:{errorMessage}", "xpack.spaces.management.deleteSpacesButton.spaceSuccessfullyDeletedNotificationMessage": "已删除 {spaceName} 空间。", - "xpack.spaces.management.editSpace.manageSpacePage.optionalDescriptionFormRowLabel": "描述(可选)", - "xpack.spaces.management.manageSpacePage.avatarLabel": "头像", "xpack.spaces.management.manageSpacePage.awesomeSpacePlaceholder": "超卓的空间", - "xpack.spaces.management.manageSpacePage.cancelButtonLabel": "取消", - "xpack.spaces.management.manageSpacePage.createSpaceButtonLabel": "创建空间", "xpack.spaces.management.manageSpacePage.createSpaceTitle": "创建一个空间", - "xpack.spaces.management.manageSpacePage.editSpaceTitle": "编辑空间", "xpack.spaces.management.manageSpacePage.errorLoadingSpaceTitle": "加载空间时出错:{message}", "xpack.spaces.management.manageSpacePage.errorSavingSpaceTitle": "保存空间时出错:{message}", - "xpack.spaces.management.manageSpacePage.hereMagicHappensPlaceholder": "实现您想要的功能。", - "xpack.spaces.management.manageSpacePage.loadingTitle": "正在加载……", "xpack.spaces.management.manageSpacePage.nameFormRowLabel": "名称", "xpack.spaces.management.manageSpacePage.spaceSuccessfullySavedNotificationMessage": "空间 “{name}” 已保存。", - "xpack.spaces.management.manageSpacePage.updateSpaceButtonLabel": "更新空间", "xpack.spaces.management.reversedSpaceBadge.reversedSpacesCanBePartiallyModifiedTooltip": "保留的空间是内置的,只能进行部分修改。", "xpack.spaces.management.secureSpaceMessage.howToAssignRoleToSpaceDescription": "想要为空间分配角色?请转到“管理”并选择 {rolesLink}。", "xpack.spaces.management.secureSpaceMessage.rolesLinkText": "角色", - "xpack.spaces.management.spaceIdentifier.editSpaceLinkText": "[编辑]", - "xpack.spaces.management.spaceIdentifier.engineeringText": "工程", - "xpack.spaces.management.spaceIdentifier.kibanaURLForEngineeringIdentifierDescription": "如果标识符为 {engineeringIdentifier},则 Kibana URL 为{nextLine} {engineeringKibanaUrl}。", - "xpack.spaces.management.spaceIdentifier.stopEditingSpaceNameLinkText": "[停止编辑]", "xpack.spaces.management.spaceIdentifier.urlIdentifierGeneratedFromSpaceNameTooltip": "URL 标识符基于空间名称生成。", "xpack.spaces.management.spaceIdentifier.urlIdentifierLabel": "URL 标识符", "xpack.spaces.management.spaceIdentifier.urlIdentifierTitle": "URL 标识符", diff --git a/x-pack/plugins/uptime/index.ts b/x-pack/plugins/uptime/index.ts index 2a88e61acf1c..959de2ede3d9 100644 --- a/x-pack/plugins/uptime/index.ts +++ b/x-pack/plugins/uptime/index.ts @@ -5,10 +5,9 @@ */ import { i18n } from '@kbn/i18n'; -import { Server } from 'hapi'; import { resolve } from 'path'; import { PLUGIN } from './common/constants'; -import { initServerWithKibana } from './server'; +import { initServerWithKibana, KibanaServer } from './server'; export const uptime = (kibana: any) => new kibana.Plugin({ @@ -33,7 +32,7 @@ export const uptime = (kibana: any) => }, home: ['plugins/uptime/register_feature'], }, - init(server: Server) { + init(server: KibanaServer) { initServerWithKibana(server); }, }); diff --git a/x-pack/plugins/uptime/public/uptime_app.tsx b/x-pack/plugins/uptime/public/uptime_app.tsx index 1927530ceb38..f4d4acd43056 100644 --- a/x-pack/plugins/uptime/public/uptime_app.tsx +++ b/x-pack/plugins/uptime/public/uptime_app.tsx @@ -140,7 +140,7 @@ const Application = (props: UptimeAppProps) => { }} > - +
diff --git a/x-pack/plugins/uptime/server/index.ts b/x-pack/plugins/uptime/server/index.ts index 1e3c022c31d5..ca7ca9a10031 100644 --- a/x-pack/plugins/uptime/server/index.ts +++ b/x-pack/plugins/uptime/server/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { initServerWithKibana } from './kibana.index'; +export { initServerWithKibana, KibanaServer } from './kibana.index'; diff --git a/x-pack/plugins/uptime/server/kibana.index.ts b/x-pack/plugins/uptime/server/kibana.index.ts index b6d9f256951d..b0225383f43c 100644 --- a/x-pack/plugins/uptime/server/kibana.index.ts +++ b/x-pack/plugins/uptime/server/kibana.index.ts @@ -4,7 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import { Request, Server } from 'hapi'; +import { PLUGIN } from '../common/constants'; import { compose } from './lib/compose/kibana'; import { initUptimeServer } from './uptime_server'; @@ -16,11 +18,41 @@ export interface KibanaRouteOptions { options: any; } -export interface KibanaServer { +export interface KibanaServer extends Server { route: (options: KibanaRouteOptions) => void; } -export const initServerWithKibana = (server: Server) => { +export const initServerWithKibana = (server: KibanaServer) => { const libs = compose(server); initUptimeServer(libs); + + const xpackMainPlugin = server.plugins.xpack_main; + xpackMainPlugin.registerFeature({ + id: PLUGIN.ID, + name: i18n.translate('xpack.uptime.featureRegistry.uptimeFeatureName', { + defaultMessage: 'Uptime', + }), + navLinkId: PLUGIN.ID, + icon: 'uptimeApp', + app: ['uptime', 'kibana'], + catalogue: ['uptime'], + privileges: { + all: { + api: ['uptime'], + savedObject: { + all: [], + read: ['config'], + }, + ui: [], + }, + read: { + api: ['uptime'], + savedObject: { + all: [], + read: ['config'], + }, + ui: [], + }, + }, + }); }; diff --git a/x-pack/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts index c0ca2fa08d81..ffd5b5e970b7 100644 --- a/x-pack/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -37,6 +37,9 @@ export class UMKibanaBackendFrameworkAdapter implements UMBackendFrameworkAdapte schema, }), path: routePath, + route: { + tags: ['access:uptime'], + }, }, plugin: uptimeGraphQLHapiPlugin, }); diff --git a/x-pack/plugins/uptime/server/rest_api/auth/is_valid.ts b/x-pack/plugins/uptime/server/rest_api/auth/is_valid.ts index 35661fe81784..15024f7c5f49 100644 --- a/x-pack/plugins/uptime/server/rest_api/auth/is_valid.ts +++ b/x-pack/plugins/uptime/server/rest_api/auth/is_valid.ts @@ -10,4 +10,7 @@ export const createIsValidRoute = (libs: UMServerLibs) => ({ method: 'GET', path: '/api/uptime/is_valid', handler: async (request: any): Promise => await libs.auth.requestIsValid(request), + options: { + tags: ['access:uptime'], + }, }); diff --git a/x-pack/plugins/uptime/server/rest_api/pings/get_all.ts b/x-pack/plugins/uptime/server/rest_api/pings/get_all.ts index d0dcc835d580..70f9a5c06119 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/get_all.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/get_all.ts @@ -22,6 +22,7 @@ export const createGetAllRoute = (libs: UMServerLibs) => ({ status: Joi.string(), }), }, + tags: ['access:uptime'], }, handler: async (request: any): Promise => { const { size, sort, dateRangeStart, dateRangeEnd, monitorId, status } = request.query; diff --git a/x-pack/plugins/xpack_main/index.js b/x-pack/plugins/xpack_main/index.js index 4ba75d950806..9ef88097e8e0 100644 --- a/x-pack/plugins/xpack_main/index.js +++ b/x-pack/plugins/xpack_main/index.js @@ -17,6 +17,7 @@ import { getLocalizationUsageCollector } from './server/lib/get_localization_usa import { xpackInfoRoute, telemetryRoute, + featuresRoute, settingsRoute, } from './server/routes/api/v1'; import { @@ -27,6 +28,7 @@ import mappings from './mappings.json'; import { i18n } from '@kbn/i18n'; export { callClusterFactory } from './server/lib/call_cluster_factory'; +import { registerOssFeatures } from './server/lib/register_oss_features'; /** * Determine if Telemetry is enabled. @@ -96,7 +98,6 @@ export const xpackMain = (kibana) => { telemetryOptedIn: null, activeSpace: null, spacesEnabled: config.get('xpack.spaces.enabled'), - userProfile: {}, }; }, hacks: [ @@ -121,11 +122,13 @@ export const xpackMain = (kibana) => { mirrorPluginStatus(server.plugins.elasticsearch, this, 'yellow', 'red'); setupXPackMain(server); + registerOssFeatures(server.plugins.xpack_main.registerFeature); // register routes xpackInfoRoute(server); telemetryRoute(server); settingsRoute(server, this.kbnServer); + featuresRoute(server); server.usage.collectorSet.register(getLocalizationUsageCollector(server)); } }); diff --git a/x-pack/plugins/xpack_main/public/services/user_profile.test.ts b/x-pack/plugins/xpack_main/public/services/user_profile.test.ts deleted file mode 100644 index 45507ab60428..000000000000 --- a/x-pack/plugins/xpack_main/public/services/user_profile.test.ts +++ /dev/null @@ -1,42 +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 { UserProfileProvider } from './user_profile'; - -describe('UserProfile', () => { - it('should return true when the specified capability is enabled', () => { - const capabilities = { - test1: true, - test2: false, - }; - - const userProfile = UserProfileProvider(capabilities); - - expect(userProfile.hasCapability('test1')).toEqual(true); - }); - - it('should return false when the specified capability is disabled', () => { - const capabilities = { - test1: true, - test2: false, - }; - - const userProfile = UserProfileProvider(capabilities); - - expect(userProfile.hasCapability('test2')).toEqual(false); - }); - - it('should return the default value when the specified capability is not defined', () => { - const capabilities = { - test1: true, - test2: false, - }; - - const userProfile = UserProfileProvider(capabilities); - - expect(userProfile.hasCapability('test3')).toEqual(true); - expect(userProfile.hasCapability('test3', false)).toEqual(false); - }); -}); diff --git a/x-pack/plugins/xpack_main/public/services/user_profile.ts b/x-pack/plugins/xpack_main/public/services/user_profile.ts deleted file mode 100644 index 09b257aa80e3..000000000000 --- a/x-pack/plugins/xpack_main/public/services/user_profile.ts +++ /dev/null @@ -1,31 +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. - */ - -interface Capabilities { - [capability: string]: boolean; -} - -export interface UserProfile { - hasCapability: (capability: string) => boolean; -} - -export function UserProfileProvider(userProfile: Capabilities) { - class UserProfileClass implements UserProfile { - private capabilities: Capabilities; - - constructor(profileData: Capabilities = {}) { - this.capabilities = { - ...profileData, - }; - } - - public hasCapability(capability: string, defaultValue: boolean = true): boolean { - return capability in this.capabilities ? this.capabilities[capability] : defaultValue; - } - } - - return new UserProfileClass(userProfile); -} diff --git a/x-pack/plugins/xpack_main/server/lib/__tests__/replace_injected_vars.js b/x-pack/plugins/xpack_main/server/lib/__tests__/replace_injected_vars.js index 26bce06adaab..263e03946cb1 100644 --- a/x-pack/plugins/xpack_main/server/lib/__tests__/replace_injected_vars.js +++ b/x-pack/plugins/xpack_main/server/lib/__tests__/replace_injected_vars.js @@ -47,7 +47,12 @@ describe('replaceInjectedVars uiExport', () => { xpackInitialInfo: { b: 1 }, - userProfile: {}, + uiCapabilities: { + mockFeature: { + mockFeatureCapability: true, + }, + catalogue: {} + }, }); sinon.assert.calledOnce(server.plugins.security.isAuthenticated); @@ -67,7 +72,12 @@ describe('replaceInjectedVars uiExport', () => { xpackInitialInfo: { b: 1 }, - userProfile: {}, + uiCapabilities: { + mockFeature: { + mockFeatureCapability: true, + }, + catalogue: {} + }, }); }); @@ -84,7 +94,12 @@ describe('replaceInjectedVars uiExport', () => { xpackInitialInfo: { b: 1 }, - userProfile: {}, + uiCapabilities: { + mockFeature: { + mockFeatureCapability: true, + }, + catalogue: {} + }, }); }); @@ -101,7 +116,12 @@ describe('replaceInjectedVars uiExport', () => { xpackInitialInfo: { b: 1 }, - userProfile: {}, + uiCapabilities: { + mockFeature: { + mockFeatureCapability: true, + }, + catalogue: {} + }, }); }); @@ -118,7 +138,12 @@ describe('replaceInjectedVars uiExport', () => { xpackInitialInfo: { b: 1 }, - userProfile: {}, + uiCapabilities: { + mockFeature: { + mockFeatureCapability: true, + }, + catalogue: {} + }, }); }); @@ -135,32 +160,53 @@ describe('replaceInjectedVars uiExport', () => { xpackInitialInfo: { b: 1 }, - userProfile: {}, + uiCapabilities: { + mockFeature: { + mockFeatureCapability: true, + }, + catalogue: {} + }, }); }); - it('sends the originalInjectedVars if not authenticated', async () => { + it('sends the originalInjectedVars augmented with UI Capabilities if not authenticated', async () => { const originalInjectedVars = { a: 1 }; const request = buildRequest(); const server = mockServer(); server.plugins.security.isAuthenticated.returns(false); const newVars = await replaceInjectedVars(originalInjectedVars, request, server); - expect(newVars).to.be(originalInjectedVars); + expect(newVars).to.eql({ + ...originalInjectedVars, + uiCapabilities: { + mockFeature: { + mockFeatureCapability: true, + }, + catalogue: {} + }, + }); }); - it('sends the originalInjectedVars if xpack info is unavailable', async () => { + it('sends the originalInjectedVars augmented with UI Capabilities if xpack info is unavailable', async () => { const originalInjectedVars = { a: 1 }; const request = buildRequest(); const server = mockServer(); server.plugins.xpack_main.info.isAvailable.returns(false); const newVars = await replaceInjectedVars(originalInjectedVars, request, server); - expect(newVars).to.be(originalInjectedVars); + expect(newVars).to.eql({ + ...originalInjectedVars, + uiCapabilities: { + mockFeature: { + mockFeatureCapability: true, + }, + catalogue: {} + }, + }); }); it('sends the originalInjectedVars (with xpackInitialInfo = undefined) if security is disabled, xpack info is unavailable', async () => { - const originalInjectedVars = { a: 1 }; + const originalInjectedVars = { a: 1, uiCapabilities: { navLinks: { foo: true }, bar: { baz: true }, catalogue: { cfoo: true } } }; const request = buildRequest(); const server = mockServer(); delete server.plugins.security; @@ -171,18 +217,35 @@ describe('replaceInjectedVars uiExport', () => { a: 1, telemetryOptedIn: null, xpackInitialInfo: undefined, - userProfile: {}, + uiCapabilities: { + navLinks: { foo: true }, + bar: { baz: true }, + mockFeature: { + mockFeatureCapability: true, + }, + catalogue: { + cfoo: true, + } + }, }); }); - it('sends the originalInjectedVars if the license check result is not available', async () => { + it('sends the originalInjectedVars augmented with UI Capabilities if the license check result is not available', async () => { const originalInjectedVars = { a: 1 }; const request = buildRequest(); const server = mockServer(); server.plugins.xpack_main.info.feature().getLicenseCheckResults.returns(undefined); const newVars = await replaceInjectedVars(originalInjectedVars, request, server); - expect(newVars).to.be(originalInjectedVars); + expect(newVars).to.eql({ + ...originalInjectedVars, + uiCapabilities: { + mockFeature: { + mockFeatureCapability: true, + }, + catalogue: {} + }, + }); }); }); @@ -196,6 +259,20 @@ function mockServer() { isAuthenticated: sinon.stub().returns(true) }, xpack_main: { + getFeatures: () => [{ + id: 'mockFeature', + name: 'Mock Feature', + privileges: { + all: { + app: [], + savedObject: { + all: [], + read: [], + }, + ui: ['mockFeatureCapability'] + } + } + }], info: { isAvailable: sinon.stub().returns(true), feature: () => ({ diff --git a/x-pack/plugins/xpack_main/server/lib/feature_registry/__snapshots__/feature_registry.test.ts.snap b/x-pack/plugins/xpack_main/server/lib/feature_registry/__snapshots__/feature_registry.test.ts.snap new file mode 100644 index 000000000000..1271f757d066 --- /dev/null +++ b/x-pack/plugins/xpack_main/server/lib/feature_registry/__snapshots__/feature_registry.test.ts.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FeatureRegistry prevents features from being registered with an ID of "catalogue" 1`] = `"child \\"id\\" fails because [\\"id\\" contains an invalid value]"`; + +exports[`FeatureRegistry prevents features from being registered with an ID of "doesn't match valid regex" 1`] = `"child \\"id\\" fails because [\\"id\\" with value \\"doesn't match valid regex\\" fails to match the required pattern: /^[a-zA-Z0-9_-]+$/]"`; + +exports[`FeatureRegistry prevents features from being registered with an ID of "management" 1`] = `"child \\"id\\" fails because [\\"id\\" contains an invalid value]"`; + +exports[`FeatureRegistry prevents features from being registered with an ID of "navLinks" 1`] = `"child \\"id\\" fails because [\\"id\\" contains an invalid value]"`; diff --git a/x-pack/plugins/xpack_main/server/lib/feature_registry/feature_registry.test.ts b/x-pack/plugins/xpack_main/server/lib/feature_registry/feature_registry.test.ts new file mode 100644 index 000000000000..cfe6719489b9 --- /dev/null +++ b/x-pack/plugins/xpack_main/server/lib/feature_registry/feature_registry.test.ts @@ -0,0 +1,338 @@ +/* + * 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 { Feature, FeatureRegistry } from './feature_registry'; + +describe('FeatureRegistry', () => { + it('allows a minimal feature to be registered', () => { + const feature: Feature = { + id: 'test-feature', + name: 'Test Feature', + app: [], + privileges: {}, + }; + + const featureRegistry = new FeatureRegistry(); + featureRegistry.register(feature); + const result = featureRegistry.getAll(); + expect(result).toHaveLength(1); + + // Should be the equal, but not the same instance (i.e., a defensive copy) + expect(result[0]).not.toBe(feature); + expect(result[0]).toEqual(feature); + }); + + it('allows a complex feature to be registered', () => { + const feature: Feature = { + id: 'test-feature', + name: 'Test Feature', + description: 'this is a rather boring feature description !@#$%^&*()_+-=\\[]{}|;\':"/.,<>?', + icon: 'addDataApp', + navLinkId: 'someNavLink', + app: ['app1', 'app2'], + validLicenses: ['standard', 'basic', 'gold', 'platinum'], + catalogue: ['foo'], + management: { + foo: ['bar'], + }, + privileges: { + all: { + grantWithBaseRead: true, + catalogue: ['foo'], + management: { + foo: ['bar'], + }, + app: ['app1'], + savedObject: { + all: ['config', 'space', 'etc'], + read: ['canvas'], + }, + api: ['someApiEndpointTag', 'anotherEndpointTag'], + ui: ['allowsFoo', 'showBar', 'showBaz'], + }, + }, + privilegesTooltip: 'some fancy tooltip', + reserved: { + privilege: { + catalogue: ['foo'], + management: { + foo: ['bar'], + }, + app: ['app1'], + savedObject: { + all: ['config', 'space', 'etc'], + read: ['canvas'], + }, + api: ['someApiEndpointTag', 'anotherEndpointTag'], + ui: ['allowsFoo', 'showBar', 'showBaz'], + }, + description: 'some completely adequate description', + }, + }; + + const featureRegistry = new FeatureRegistry(); + featureRegistry.register(feature); + const result = featureRegistry.getAll(); + expect(result).toHaveLength(1); + + // Should be the equal, but not the same instance (i.e., a defensive copy) + expect(result[0]).not.toBe(feature); + expect(result[0]).toEqual(feature); + }); + + it(`does not allow duplicate features to be registered`, () => { + const feature: Feature = { + id: 'test-feature', + name: 'Test Feature', + app: [], + privileges: {}, + }; + + const duplicateFeature: Feature = { + id: 'test-feature', + name: 'Duplicate Test Feature', + app: [], + privileges: {}, + }; + + const featureRegistry = new FeatureRegistry(); + featureRegistry.register(feature); + + expect(() => featureRegistry.register(duplicateFeature)).toThrowErrorMatchingInlineSnapshot( + `"Feature with id test-feature is already registered."` + ); + }); + + ['catalogue', 'management', 'navLinks', `doesn't match valid regex`].forEach(prohibitedId => { + it(`prevents features from being registered with an ID of "${prohibitedId}"`, () => { + const featureRegistry = new FeatureRegistry(); + expect(() => + featureRegistry.register({ + id: prohibitedId, + name: 'some feature', + app: [], + privileges: {}, + }) + ).toThrowErrorMatchingSnapshot(); + }); + }); + + it('prevents features from being registered with invalid privilege names', () => { + const feature: Feature = { + id: 'test-feature', + name: 'Test Feature', + app: ['app1', 'app2'], + privileges: { + foo: { + app: ['app1', 'app2'], + savedObject: { + all: ['config', 'space', 'etc'], + read: ['canvas'], + }, + api: ['someApiEndpointTag', 'anotherEndpointTag'], + ui: ['allowsFoo', 'showBar', 'showBaz'], + }, + }, + }; + + const featureRegistry = new FeatureRegistry(); + expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( + `"child \\"privileges\\" fails because [\\"foo\\" is not allowed]"` + ); + }); + + it(`prevents privileges from specifying app entries that don't exist at the root level`, () => { + const feature: Feature = { + id: 'test-feature', + name: 'Test Feature', + app: ['bar'], + privileges: { + all: { + savedObject: { + all: [], + read: [], + }, + ui: [], + app: ['foo', 'bar', 'baz'], + }, + }, + }; + + const featureRegistry = new FeatureRegistry(); + + expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( + `"Feature privilege test-feature.all has unknown app entries: foo, baz"` + ); + }); + + it(`prevents reserved privileges from specifying app entries that don't exist at the root level`, () => { + const feature: Feature = { + id: 'test-feature', + name: 'Test Feature', + app: ['bar'], + privileges: {}, + reserved: { + description: 'something', + privilege: { + savedObject: { + all: [], + read: [], + }, + ui: [], + app: ['foo', 'bar', 'baz'], + }, + }, + }; + + const featureRegistry = new FeatureRegistry(); + + expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( + `"Feature privilege test-feature.reserved has unknown app entries: foo, baz"` + ); + }); + + it(`prevents privileges from specifying catalogue entries that don't exist at the root level`, () => { + const feature: Feature = { + id: 'test-feature', + name: 'Test Feature', + app: [], + catalogue: ['bar'], + privileges: { + all: { + catalogue: ['foo', 'bar', 'baz'], + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + }; + + const featureRegistry = new FeatureRegistry(); + + expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( + `"Feature privilege test-feature.all has unknown catalogue entries: foo, baz"` + ); + }); + + it(`prevents reserved privileges from specifying catalogue entries that don't exist at the root level`, () => { + const feature: Feature = { + id: 'test-feature', + name: 'Test Feature', + app: [], + catalogue: ['bar'], + privileges: {}, + reserved: { + description: 'something', + privilege: { + catalogue: ['foo', 'bar', 'baz'], + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + }; + + const featureRegistry = new FeatureRegistry(); + + expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( + `"Feature privilege test-feature.reserved has unknown catalogue entries: foo, baz"` + ); + }); + + it(`prevents privileges from specifying management sections that don't exist at the root level`, () => { + const feature: Feature = { + id: 'test-feature', + name: 'Test Feature', + app: [], + catalogue: ['bar'], + management: { + kibana: ['hey'], + }, + privileges: { + all: { + catalogue: ['bar'], + management: { + elasticsearch: ['hey'], + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + }; + + const featureRegistry = new FeatureRegistry(); + + expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( + `"Feature privilege test-feature.all has unknown management section: elasticsearch"` + ); + }); + + it(`prevents reserved privileges from specifying management entries that don't exist at the root level`, () => { + const feature: Feature = { + id: 'test-feature', + name: 'Test Feature', + app: [], + catalogue: ['bar'], + management: { + kibana: ['hey'], + }, + privileges: {}, + reserved: { + description: 'something', + privilege: { + catalogue: ['bar'], + management: { + kibana: ['hey-there'], + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + }; + + const featureRegistry = new FeatureRegistry(); + + expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( + `"Feature privilege test-feature.reserved has unknown management entries for section kibana: hey-there"` + ); + }); + + it('cannot register feature after getAll has been called', () => { + const feature1: Feature = { + id: 'test-feature', + name: 'Test Feature', + app: [], + privileges: {}, + }; + const feature2: Feature = { + id: 'test-feature-2', + name: 'Test Feature 2', + app: [], + privileges: {}, + }; + + const featureRegistry = new FeatureRegistry(); + featureRegistry.register(feature1); + featureRegistry.getAll(); + expect(() => { + featureRegistry.register(feature2); + }).toThrowErrorMatchingInlineSnapshot(`"Features are locked, can't register new features"`); + }); +}); diff --git a/x-pack/plugins/xpack_main/server/lib/feature_registry/feature_registry.ts b/x-pack/plugins/xpack_main/server/lib/feature_registry/feature_registry.ts new file mode 100644 index 000000000000..e536529f7d6d --- /dev/null +++ b/x-pack/plugins/xpack_main/server/lib/feature_registry/feature_registry.ts @@ -0,0 +1,196 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Joi from 'joi'; +import { cloneDeep, difference } from 'lodash'; +import { UICapabilities } from 'ui/capabilities'; + +export interface FeatureKibanaPrivileges { + grantWithBaseRead?: boolean; + management?: { + [sectionId: string]: string[]; + }; + catalogue?: string[]; + api?: string[]; + app?: string[]; + savedObject: { + all: string[]; + read: string[]; + }; + ui: string[]; +} + +type PrivilegesSet = Record; + +export type FeatureWithAllOrReadPrivileges = Feature<{ + all?: FeatureKibanaPrivileges; + read?: FeatureKibanaPrivileges; +}>; + +export interface Feature = PrivilegesSet> { + id: string; + name: string; + validLicenses?: Array<'basic' | 'standard' | 'gold' | 'platinum'>; + icon?: string; + description?: string; + navLinkId?: string; + app: string[]; + management?: { + [sectionId: string]: string[]; + }; + catalogue?: string[]; + privileges: TPrivileges; + privilegesTooltip?: string; + reserved?: { + privilege: FeatureKibanaPrivileges; + description: string; + }; +} + +// Each feature gets its own property on the UICapabilities object, +// but that object has a few built-in properties which should not be overwritten. +const prohibitedFeatureIds: Array = ['catalogue', 'management', 'navLinks']; + +const featurePrivilegePartRegex = /^[a-zA-Z0-9_-]+$/; +const managementSectionIdRegex = /^[a-zA-Z0-9_-]+$/; +export const uiCapabilitiesRegex = /^[a-zA-Z0-9:_-]+$/; + +const managementSchema = Joi.object().pattern( + managementSectionIdRegex, + Joi.array().items(Joi.string()) +); +const catalogueSchema = Joi.array().items(Joi.string()); + +const privilegeSchema = Joi.object({ + grantWithBaseRead: Joi.bool(), + management: managementSchema, + catalogue: catalogueSchema, + api: Joi.array().items(Joi.string()), + app: Joi.array().items(Joi.string()), + savedObject: Joi.object({ + all: Joi.array() + .items(Joi.string()) + .required(), + read: Joi.array() + .items(Joi.string()) + .required(), + }).required(), + ui: Joi.array() + .items(Joi.string().regex(uiCapabilitiesRegex)) + .required(), +}); + +const schema = Joi.object({ + id: Joi.string() + .regex(featurePrivilegePartRegex) + .invalid(...prohibitedFeatureIds) + .required(), + name: Joi.string().required(), + validLicenses: Joi.array().items(Joi.string().valid('basic', 'standard', 'gold', 'platinum')), + icon: Joi.string(), + description: Joi.string(), + navLinkId: Joi.string(), + app: Joi.array() + .items(Joi.string()) + .required(), + management: managementSchema, + catalogue: catalogueSchema, + privileges: Joi.object({ + all: privilegeSchema, + read: privilegeSchema, + }).required(), + privilegesTooltip: Joi.string(), + reserved: Joi.object({ + privilege: privilegeSchema.required(), + description: Joi.string().required(), + }), +}); + +export class FeatureRegistry { + private locked = false; + private features: Record = {}; + + public register(feature: FeatureWithAllOrReadPrivileges) { + if (this.locked) { + throw new Error(`Features are locked, can't register new features`); + } + + validateFeature(feature); + + if (feature.id in this.features) { + throw new Error(`Feature with id ${feature.id} is already registered.`); + } + + this.features[feature.id] = feature as Feature; + } + + public getAll(): Feature[] { + this.locked = true; + return cloneDeep(Object.values(this.features)); + } +} + +function validateFeature(feature: FeatureWithAllOrReadPrivileges) { + const validateResult = Joi.validate(feature, schema); + if (validateResult.error) { + throw validateResult.error; + } + // the following validation can't be enforced by the Joi schema, since it'd require us looking "up" the object graph for the list of valid value, which they explicitly forbid. + const { app = [], management = {}, catalogue = [] } = feature; + + const privilegeEntries = [...Object.entries(feature.privileges)]; + if (feature.reserved) { + privilegeEntries.push(['reserved', feature.reserved.privilege]); + } + + privilegeEntries.forEach(([privilegeId, privilegeDefinition]) => { + if (!privilegeDefinition) { + throw new Error('Privilege definition may not be null or undefined'); + } + + const unknownAppEntries = difference(privilegeDefinition.app || [], app); + if (unknownAppEntries.length > 0) { + throw new Error( + `Feature privilege ${ + feature.id + }.${privilegeId} has unknown app entries: ${unknownAppEntries.join(', ')}` + ); + } + + const unknownCatalogueEntries = difference(privilegeDefinition.catalogue || [], catalogue); + if (unknownCatalogueEntries.length > 0) { + throw new Error( + `Feature privilege ${ + feature.id + }.${privilegeId} has unknown catalogue entries: ${unknownCatalogueEntries.join(', ')}` + ); + } + + Object.entries(privilegeDefinition.management || {}).forEach( + ([managementSectionId, managementEntry]) => { + if (!management[managementSectionId]) { + throw new Error( + `Feature privilege ${ + feature.id + }.${privilegeId} has unknown management section: ${managementSectionId}` + ); + } + + const unknownSectionEntries = difference(managementEntry, management[managementSectionId]); + + if (unknownSectionEntries.length > 0) { + throw new Error( + `Feature privilege ${ + feature.id + }.${privilegeId} has unknown management entries for section ${managementSectionId}: ${unknownSectionEntries.join( + ', ' + )}` + ); + } + } + ); + }); +} diff --git a/x-pack/plugins/xpack_main/server/lib/feature_registry/index.ts b/x-pack/plugins/xpack_main/server/lib/feature_registry/index.ts new file mode 100644 index 000000000000..8cbff5df3b2b --- /dev/null +++ b/x-pack/plugins/xpack_main/server/lib/feature_registry/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 { + Feature, + FeatureKibanaPrivileges, + FeatureRegistry, + FeatureWithAllOrReadPrivileges, + uiCapabilitiesRegex, +} from './feature_registry'; diff --git a/x-pack/plugins/xpack_main/server/lib/populate_ui_capabilities.test.ts b/x-pack/plugins/xpack_main/server/lib/populate_ui_capabilities.test.ts new file mode 100644 index 000000000000..7c08356e27b8 --- /dev/null +++ b/x-pack/plugins/xpack_main/server/lib/populate_ui_capabilities.test.ts @@ -0,0 +1,302 @@ +/* + * 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 { UICapabilities } from 'ui/capabilities'; +import { Feature } from './feature_registry'; +import { populateUICapabilities } from './populate_ui_capabilities'; + +function getMockXpackMainPlugin(features: Feature[]) { + return { + getFeatures: () => features, + }; +} + +function getMockOriginalInjectedVars() { + return { + uiCapabilities: { + navLinks: { + foo: true, + bar: true, + }, + management: {}, + catalogue: { + fooEntry: true, + barEntry: true, + }, + feature: { + someCapability: true, + }, + otherFeature: {}, + }, + }; +} + +function createFeaturePrivilege(key: string, capabilities: string[] = []) { + return { + [key]: { + savedObject: { + all: [], + read: [], + }, + app: [], + ui: [...capabilities], + }, + }; +} + +describe('populateUICapabilities', () => { + it('handles no original uiCapabilites and no registered features gracefully', () => { + const xpackMainPlugin = getMockXpackMainPlugin([]); + + expect(populateUICapabilities(xpackMainPlugin, {} as UICapabilities)).toEqual({}); + }); + + it('returns the original uiCapabilities untouched when no features are registered', () => { + const xpackMainPlugin = getMockXpackMainPlugin([]); + const originalInjectedVars = getMockOriginalInjectedVars(); + + expect(populateUICapabilities(xpackMainPlugin, originalInjectedVars.uiCapabilities)).toEqual({ + feature: { + someCapability: true, + }, + navLinks: { + foo: true, + bar: true, + }, + management: {}, + catalogue: { + fooEntry: true, + barEntry: true, + }, + otherFeature: {}, + }); + }); + + it('handles features with no registered capabilities', () => { + const xpackMainPlugin = getMockXpackMainPlugin([ + { + id: 'newFeature', + name: 'my new feature', + app: ['bar-app'], + privileges: { + ...createFeaturePrivilege('all'), + }, + }, + ]); + const originalInjectedVars = getMockOriginalInjectedVars(); + + expect(populateUICapabilities(xpackMainPlugin, originalInjectedVars.uiCapabilities)).toEqual({ + feature: { + someCapability: true, + }, + navLinks: { + foo: true, + bar: true, + }, + management: {}, + catalogue: { + fooEntry: true, + barEntry: true, + }, + newFeature: {}, + otherFeature: {}, + }); + }); + + it('augments the original uiCapabilities with registered feature capabilities', () => { + const xpackMainPlugin = getMockXpackMainPlugin([ + { + id: 'newFeature', + name: 'my new feature', + navLinkId: 'newFeatureNavLink', + app: ['bar-app'], + privileges: { + ...createFeaturePrivilege('all', ['capability1', 'capability2']), + }, + }, + ]); + const originalInjectedVars = getMockOriginalInjectedVars(); + + expect(populateUICapabilities(xpackMainPlugin, originalInjectedVars.uiCapabilities)).toEqual({ + feature: { + someCapability: true, + }, + navLinks: { + foo: true, + bar: true, + }, + management: {}, + catalogue: { + fooEntry: true, + barEntry: true, + }, + newFeature: { + capability1: true, + capability2: true, + }, + otherFeature: {}, + }); + }); + + it('combines catalogue entries from multiple features', () => { + const xpackMainPlugin = getMockXpackMainPlugin([ + { + id: 'newFeature', + name: 'my new feature', + navLinkId: 'newFeatureNavLink', + app: ['bar-app'], + catalogue: ['anotherFooEntry', 'anotherBarEntry'], + privileges: { + ...createFeaturePrivilege('foo', ['capability1', 'capability2']), + ...createFeaturePrivilege('bar', ['capability3', 'capability4']), + ...createFeaturePrivilege('baz'), + }, + }, + ]); + const originalInjectedVars = getMockOriginalInjectedVars(); + + expect(populateUICapabilities(xpackMainPlugin, originalInjectedVars.uiCapabilities)).toEqual({ + feature: { + someCapability: true, + }, + navLinks: { + foo: true, + bar: true, + }, + management: {}, + catalogue: { + fooEntry: true, + anotherFooEntry: true, + barEntry: true, + anotherBarEntry: true, + }, + newFeature: { + capability1: true, + capability2: true, + capability3: true, + capability4: true, + }, + otherFeature: {}, + }); + }); + + it(`merges capabilities from all feature privileges`, () => { + const xpackMainPlugin = getMockXpackMainPlugin([ + { + id: 'newFeature', + name: 'my new feature', + navLinkId: 'newFeatureNavLink', + app: ['bar-app'], + privileges: { + ...createFeaturePrivilege('foo', ['capability1', 'capability2']), + ...createFeaturePrivilege('bar', ['capability3', 'capability4']), + ...createFeaturePrivilege('baz', ['capability1', 'capability5']), + }, + }, + ]); + const originalInjectedVars = getMockOriginalInjectedVars(); + + expect(populateUICapabilities(xpackMainPlugin, originalInjectedVars.uiCapabilities)).toEqual({ + feature: { + someCapability: true, + }, + navLinks: { + foo: true, + bar: true, + }, + management: {}, + catalogue: { + fooEntry: true, + barEntry: true, + }, + newFeature: { + capability1: true, + capability2: true, + capability3: true, + capability4: true, + capability5: true, + }, + otherFeature: {}, + }); + }); + + it('supports merging multiple features with multiple privileges each', () => { + const xpackMainPlugin = getMockXpackMainPlugin([ + { + id: 'newFeature', + name: 'my new feature', + navLinkId: 'newFeatureNavLink', + app: ['bar-app'], + privileges: { + ...createFeaturePrivilege('foo', ['capability1', 'capability2']), + ...createFeaturePrivilege('bar', ['capability3', 'capability4']), + ...createFeaturePrivilege('baz', ['capability1', 'capability5']), + }, + }, + { + id: 'anotherNewFeature', + name: 'another new feature', + app: ['bar-app'], + privileges: { + ...createFeaturePrivilege('foo', ['capability1', 'capability2']), + ...createFeaturePrivilege('bar', ['capability3', 'capability4']), + }, + }, + { + id: 'yetAnotherNewFeature', + name: 'yet another new feature', + navLinkId: 'yetAnotherNavLink', + app: ['bar-app'], + privileges: { + ...createFeaturePrivilege('all', ['capability1', 'capability2']), + ...createFeaturePrivilege('read', []), + ...createFeaturePrivilege('somethingInBetween', [ + 'something1', + 'something2', + 'something3', + ]), + }, + }, + ]); + const originalInjectedVars = getMockOriginalInjectedVars(); + + expect(populateUICapabilities(xpackMainPlugin, originalInjectedVars.uiCapabilities)).toEqual({ + anotherNewFeature: { + capability1: true, + capability2: true, + capability3: true, + capability4: true, + }, + feature: { + someCapability: true, + }, + navLinks: { + foo: true, + bar: true, + }, + management: {}, + catalogue: { + fooEntry: true, + barEntry: true, + }, + newFeature: { + capability1: true, + capability2: true, + capability3: true, + capability4: true, + capability5: true, + }, + otherFeature: {}, + yetAnotherNewFeature: { + capability1: true, + capability2: true, + something1: true, + something2: true, + something3: true, + }, + }); + }); +}); diff --git a/x-pack/plugins/xpack_main/server/lib/populate_ui_capabilities.ts b/x-pack/plugins/xpack_main/server/lib/populate_ui_capabilities.ts new file mode 100644 index 000000000000..4d7548c38725 --- /dev/null +++ b/x-pack/plugins/xpack_main/server/lib/populate_ui_capabilities.ts @@ -0,0 +1,84 @@ +/* + * 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 _ from 'lodash'; +import { UICapabilities } from 'ui/capabilities'; +import { Feature } from '../../types'; + +const ELIGIBLE_FLAT_MERGE_KEYS = ['catalogue']; + +interface FeatureCapabilities { + [featureId: string]: Record; +} + +export function populateUICapabilities( + xpackMainPlugin: Record, + uiCapabilities: UICapabilities +): UICapabilities { + const features: Feature[] = xpackMainPlugin.getFeatures(); + + const featureCapabilities: FeatureCapabilities[] = features.map(getCapabilitiesFromFeature); + + return mergeCapabilities(uiCapabilities || {}, ...featureCapabilities); +} + +function getCapabilitiesFromFeature(feature: Feature): FeatureCapabilities { + const UIFeatureCapabilities: FeatureCapabilities = { + catalogue: {}, + [feature.id]: {}, + }; + + if (feature.catalogue) { + UIFeatureCapabilities.catalogue = { + ...UIFeatureCapabilities.catalogue, + ...feature.catalogue.reduce( + (acc, capability) => ({ + ...acc, + [capability]: true, + }), + {} + ), + }; + } + + Object.values(feature.privileges).forEach(privilege => { + UIFeatureCapabilities[feature.id] = { + ...UIFeatureCapabilities[feature.id], + ...privilege.ui.reduce( + (privilegeAcc, capability) => ({ + ...privilegeAcc, + [capability]: true, + }), + {} + ), + }; + }); + + return UIFeatureCapabilities; +} + +function mergeCapabilities( + originalCapabilities: UICapabilities, + ...allFeatureCapabilities: FeatureCapabilities[] +): UICapabilities { + return allFeatureCapabilities.reduce((acc, capabilities) => { + const mergableCapabilities: UICapabilities = _.omit(capabilities, ...ELIGIBLE_FLAT_MERGE_KEYS); + + const mergedFeatureCapabilities = { + ...mergableCapabilities, + ...acc, + }; + + ELIGIBLE_FLAT_MERGE_KEYS.forEach(key => { + mergedFeatureCapabilities[key] = { + ...mergedFeatureCapabilities[key], + ...capabilities[key], + }; + }); + + return mergedFeatureCapabilities; + }, originalCapabilities); +} diff --git a/x-pack/plugins/xpack_main/server/lib/register_oss_features.ts b/x-pack/plugins/xpack_main/server/lib/register_oss_features.ts new file mode 100644 index 000000000000..fa44808f18d5 --- /dev/null +++ b/x-pack/plugins/xpack_main/server/lib/register_oss_features.ts @@ -0,0 +1,224 @@ +/* + * 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 { Feature } from './feature_registry'; + +const kibanaFeatures: Feature[] = [ + { + id: 'discover', + name: i18n.translate('xpack.main.featureRegistry.discoverFeatureName', { + defaultMessage: 'Discover', + }), + icon: 'discoverApp', + navLinkId: 'kibana:discover', + app: ['kibana'], + catalogue: ['discover'], + privileges: { + all: { + savedObject: { + all: ['search', 'url'], + read: ['config', 'index-pattern'], + }, + ui: ['show', 'createShortUrl', 'save'], + }, + read: { + savedObject: { + all: [], + read: ['config', 'index-pattern', 'search', 'url'], + }, + ui: ['show'], + }, + }, + }, + { + id: 'visualize', + name: i18n.translate('xpack.main.featureRegistry.visualizeFeatureName', { + defaultMessage: 'Visualize', + }), + icon: 'visualizeApp', + navLinkId: 'kibana:visualize', + app: ['kibana'], + catalogue: ['visualize'], + privileges: { + all: { + savedObject: { + all: ['visualization', 'url'], + read: ['config', 'index-pattern', 'search'], + }, + ui: ['show', 'createShortUrl', 'delete', 'save'], + }, + read: { + savedObject: { + all: [], + read: ['config', 'index-pattern', 'search', 'visualization'], + }, + ui: ['show'], + }, + }, + }, + { + id: 'dashboard', + name: i18n.translate('xpack.main.featureRegistry.dashboardFeatureName', { + defaultMessage: 'Dashboard', + }), + icon: 'dashboardApp', + navLinkId: 'kibana:dashboard', + app: ['kibana'], + catalogue: ['dashboard'], + privileges: { + all: { + savedObject: { + all: ['dashboard', 'url'], + read: [ + 'config', + 'index-pattern', + 'search', + 'visualization', + 'timelion-sheet', + 'canvas-workpad', + ], + }, + ui: ['createNew', 'show', 'showWriteControls'], + }, + read: { + savedObject: { + all: [], + read: [ + 'config', + 'index-pattern', + 'search', + 'visualization', + 'timelion-sheet', + 'canvas-workpad', + 'dashboard', + ], + }, + ui: ['show'], + }, + }, + }, + { + id: 'dev_tools', + name: i18n.translate('xpack.main.featureRegistry.devToolsFeatureName', { + defaultMessage: 'Dev Tools', + }), + icon: 'devToolsApp', + navLinkId: 'kibana:dev_tools', + app: ['kibana'], + catalogue: ['console', 'searchprofiler', 'grokdebugger'], + privileges: { + all: { + api: ['console'], + savedObject: { + all: [], + read: ['config'], + }, + ui: ['show'], + }, + read: { + api: ['console'], + savedObject: { + all: [], + read: ['config'], + }, + ui: ['show'], + }, + }, + privilegesTooltip: i18n.translate('xpack.main.featureRegistry.devToolsPrivilegesTooltip', { + defaultMessage: + 'User should also be granted the appropriate Elasticsearch cluster and index privileges', + }), + }, + { + id: 'advancedSettings', + name: i18n.translate('xpack.main.featureRegistry.advancedSettingsFeatureName', { + defaultMessage: 'Advanced Settings', + }), + icon: 'advancedSettingsApp', + app: ['kibana'], + catalogue: ['advanced_settings'], + management: { + kibana: ['settings'], + }, + privileges: { + all: { + savedObject: { + all: ['config'], + read: [], + }, + ui: ['save'], + }, + read: { + savedObject: { + all: [], + read: ['config'], + }, + ui: [], + }, + }, + }, + { + id: 'indexPatterns', + name: i18n.translate('xpack.main.featureRegistry.indexPatternFeatureName', { + defaultMessage: 'Index Pattern Management', + }), + icon: 'indexPatternApp', + app: ['kibana'], + catalogue: ['index_patterns'], + management: { + kibana: ['index_patterns'], + }, + privileges: { + all: { + savedObject: { + all: ['index-pattern'], + read: ['config'], + }, + ui: ['createNew'], + }, + read: { + savedObject: { + all: [], + read: ['index-pattern', 'config'], + }, + ui: [], + }, + }, + }, +]; + +const timelionFeatures: Feature[] = [ + { + id: 'timelion', + name: 'Timelion', + icon: 'timelionApp', + navLinkId: 'timelion', + app: ['timelion', 'kibana'], + catalogue: ['timelion'], + privileges: { + all: { + savedObject: { + all: ['timelion-sheet'], + read: ['config', 'index-pattern'], + }, + ui: ['save'], + }, + read: { + savedObject: { + all: [], + read: ['config', 'index-pattern', 'timelion-sheet'], + }, + ui: [], + }, + }, + }, +]; + +export function registerOssFeatures(registerFeature: (feature: Feature) => void) { + for (const feature of [...kibanaFeatures, ...timelionFeatures]) { + registerFeature(feature); + } +} diff --git a/x-pack/plugins/xpack_main/server/lib/replace_injected_vars.js b/x-pack/plugins/xpack_main/server/lib/replace_injected_vars.js index 3f945fe962fd..2a1d34e0299e 100644 --- a/x-pack/plugins/xpack_main/server/lib/replace_injected_vars.js +++ b/x-pack/plugins/xpack_main/server/lib/replace_injected_vars.js @@ -5,15 +5,22 @@ */ import { getTelemetryOptIn } from './get_telemetry_opt_in'; -import { buildUserProfile } from './user_profile_registry'; +import { populateUICapabilities } from './populate_ui_capabilities'; export async function replaceInjectedVars(originalInjectedVars, request, server) { const xpackInfo = server.plugins.xpack_main.info; - const withXpackInfo = async () => ({ + + const originalInjectedVarsWithUICapabilities = { ...originalInjectedVars, + uiCapabilities: { + ...populateUICapabilities(server.plugins.xpack_main, originalInjectedVars.uiCapabilities), + } + }; + + const withXpackInfo = async () => ({ + ...originalInjectedVarsWithUICapabilities, telemetryOptedIn: await getTelemetryOptIn(request), xpackInitialInfo: xpackInfo.isAvailable() ? xpackInfo.toJSON() : undefined, - userProfile: await buildUserProfile(request), }); // security feature is disabled @@ -23,7 +30,7 @@ export async function replaceInjectedVars(originalInjectedVars, request, server) // not enough license info to make decision one way or another if (!xpackInfo.isAvailable() || !xpackInfo.feature('security').getLicenseCheckResults()) { - return originalInjectedVars; + return originalInjectedVarsWithUICapabilities; } // authentication is not a thing you can do @@ -33,7 +40,7 @@ export async function replaceInjectedVars(originalInjectedVars, request, server) // request is not authenticated if (!await server.plugins.security.isAuthenticated(request)) { - return originalInjectedVars; + return originalInjectedVarsWithUICapabilities; } // plugin enabled, license is appropriate, request is authenticated diff --git a/x-pack/plugins/xpack_main/server/lib/setup_xpack_main.js b/x-pack/plugins/xpack_main/server/lib/setup_xpack_main.js index 125e481c384a..bd93283739d9 100644 --- a/x-pack/plugins/xpack_main/server/lib/setup_xpack_main.js +++ b/x-pack/plugins/xpack_main/server/lib/setup_xpack_main.js @@ -6,6 +6,7 @@ import { injectXPackInfoSignature } from './inject_xpack_info_signature'; import { XPackInfo } from './xpack_info'; +import { FeatureRegistry } from './feature_registry'; /** * Setup the X-Pack Main plugin. This is fired every time that the Elasticsearch plugin becomes Green. @@ -24,6 +25,10 @@ export function setupXPackMain(server) { server.expose('createXPackInfo', (options) => new XPackInfo(server, options)); server.ext('onPreResponse', (request, h) => injectXPackInfoSignature(info, request, h)); + const featureRegistry = new FeatureRegistry(); + server.expose('registerFeature', (feature) => featureRegistry.register(feature)); + server.expose('getFeatures', () => featureRegistry.getAll()); + const setPluginStatus = () => { if (info.isAvailable()) { server.plugins.xpack_main.status.green('Ready'); diff --git a/x-pack/plugins/xpack_main/server/lib/user_profile_registry.test.ts b/x-pack/plugins/xpack_main/server/lib/user_profile_registry.test.ts deleted file mode 100644 index 22a0b58b60c2..000000000000 --- a/x-pack/plugins/xpack_main/server/lib/user_profile_registry.test.ts +++ /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 { - buildUserProfile, - registerUserProfileCapabilityFactory, - removeAllFactories, -} from './user_profile_registry'; - -describe('UserProfileRegistry', () => { - beforeEach(() => removeAllFactories()); - - it('should produce an empty user profile', async () => { - expect(await buildUserProfile(null)).toEqual({}); - }); - - it('should accumulate the results of all registered factories', async () => { - registerUserProfileCapabilityFactory(async () => ({ - foo: true, - bar: false, - })); - - registerUserProfileCapabilityFactory(async () => ({ - anotherCapability: true, - })); - - expect(await buildUserProfile(null)).toEqual({ - foo: true, - bar: false, - anotherCapability: true, - }); - }); -}); diff --git a/x-pack/plugins/xpack_main/server/lib/user_profile_registry.ts b/x-pack/plugins/xpack_main/server/lib/user_profile_registry.ts deleted file mode 100644 index 417341165fde..000000000000 --- a/x-pack/plugins/xpack_main/server/lib/user_profile_registry.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export type CapabilityFactory = (request: any) => Promise<{ [capability: string]: boolean }>; - -let factories: CapabilityFactory[] = []; - -export function removeAllFactories() { - factories = []; -} - -export function registerUserProfileCapabilityFactory(factory: CapabilityFactory) { - factories.push(factory); -} - -export async function buildUserProfile(request: any) { - const factoryPromises = factories.map(async factory => ({ - ...(await factory(request)), - })); - - const factoryResults = await Promise.all(factoryPromises); - - return factoryResults.reduce((acc, capabilities) => { - return { - ...acc, - ...capabilities, - }; - }, {}); -} diff --git a/x-pack/plugins/xpack_main/server/lib/xpack_info_license.d.ts b/x-pack/plugins/xpack_main/server/lib/xpack_info_license.d.ts index 0c6b7b0eaafa..ab09e0d73b80 100644 --- a/x-pack/plugins/xpack_main/server/lib/xpack_info_license.d.ts +++ b/x-pack/plugins/xpack_main/server/lib/xpack_info_license.d.ts @@ -12,7 +12,7 @@ export declare class XPackInfoLicense { public getUid(): string | undefined; public isActive(): boolean; public getExpiryDateInMillis(): number | undefined; - public isOneOf(candidateLicenses: string): boolean; + public isOneOf(candidateLicenses: string[]): boolean; public getType(): LicenseType | undefined; public getMode(): string | undefined; public isActiveLicense(typeChecker: (mode: string) => boolean): boolean; diff --git a/x-pack/plugins/xpack_main/server/routes/api/v1/features/__snapshots__/features.test.ts.snap b/x-pack/plugins/xpack_main/server/routes/api/v1/features/__snapshots__/features.test.ts.snap new file mode 100644 index 000000000000..856ca089d878 --- /dev/null +++ b/x-pack/plugins/xpack_main/server/routes/api/v1/features/__snapshots__/features.test.ts.snap @@ -0,0 +1,34 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GET /api/features/v1 does not return features that arent allowed by current license 1`] = ` +Array [ + Object { + "app": Array [], + "id": "feature_1", + "name": "Feature 1", + "privileges": Object {}, + }, +] +`; + +exports[`GET /api/features/v1 returns a list of available features 1`] = ` +Array [ + Object { + "app": Array [], + "id": "feature_1", + "name": "Feature 1", + "privileges": Object {}, + }, + Object { + "app": Array [ + "bar-app", + ], + "id": "licensed_feature", + "name": "Licensed Feature", + "privileges": Object {}, + "validLicenses": Array [ + "gold", + ], + }, +] +`; diff --git a/x-pack/plugins/xpack_main/server/routes/api/v1/features/features.test.ts b/x-pack/plugins/xpack_main/server/routes/api/v1/features/features.test.ts new file mode 100644 index 000000000000..c66f15b4698f --- /dev/null +++ b/x-pack/plugins/xpack_main/server/routes/api/v1/features/features.test.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Server } from 'hapi'; +import { KibanaConfig } from 'src/legacy/server/kbn_server'; +import { FeatureRegistry } from '../../../../lib/feature_registry'; +// @ts-ignore +import { setupXPackMain } from '../../../../lib/setup_xpack_main'; +import { featuresRoute } from './features'; + +let server: Server; +let currentLicenseLevel: string = 'gold'; + +describe('GET /api/features/v1', () => { + beforeAll(() => { + server = new Server(); + + const config: Record = {}; + server.config = () => { + return { + get: (key: string) => { + return config[key]; + }, + } as KibanaConfig; + }; + const featureRegistry = new FeatureRegistry(); + // @ts-ignore + server.plugins.xpack_main = { + getFeatures: () => featureRegistry.getAll(), + info: { + // @ts-ignore + license: { + isOneOf: (candidateLicenses: string[]) => { + return candidateLicenses.includes(currentLicenseLevel); + }, + }, + }, + }; + + featuresRoute(server); + + featureRegistry.register({ + id: 'feature_1', + name: 'Feature 1', + app: [], + privileges: {}, + }); + + featureRegistry.register({ + id: 'licensed_feature', + name: 'Licensed Feature', + app: ['bar-app'], + validLicenses: ['gold'], + privileges: {}, + }); + }); + + it('returns a list of available features', async () => { + const response = await server.inject({ + url: '/api/features/v1', + }); + + expect(response.statusCode).toEqual(200); + expect(JSON.parse(response.payload)).toMatchSnapshot(); + }); + + it(`does not return features that arent allowed by current license`, async () => { + currentLicenseLevel = 'basic'; + + const response = await server.inject({ + url: '/api/features/v1', + }); + + expect(response.statusCode).toEqual(200); + expect(JSON.parse(response.payload)).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/xpack_main/server/routes/api/v1/features/features.ts b/x-pack/plugins/xpack_main/server/routes/api/v1/features/features.ts new file mode 100644 index 000000000000..f54c7990217f --- /dev/null +++ b/x-pack/plugins/xpack_main/server/routes/api/v1/features/features.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Feature } from '../../../../../types'; + +export function featuresRoute(server: Record) { + server.route({ + path: '/api/features/v1', + method: 'GET', + async handler(request: Record) { + const xpackInfo = server.plugins.xpack_main.info; + + const allFeatures: Feature[] = server.plugins.xpack_main.getFeatures(); + + return allFeatures.filter( + feature => + !feature.validLicenses || + !feature.validLicenses.length || + xpackInfo.license.isOneOf(feature.validLicenses) + ); + }, + }); +} diff --git a/x-pack/plugins/xpack_main/server/routes/api/v1/features/index.ts b/x-pack/plugins/xpack_main/server/routes/api/v1/features/index.ts new file mode 100644 index 000000000000..856f0b583d78 --- /dev/null +++ b/x-pack/plugins/xpack_main/server/routes/api/v1/features/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 { featuresRoute } from './features'; diff --git a/x-pack/plugins/xpack_main/server/routes/api/v1/index.js b/x-pack/plugins/xpack_main/server/routes/api/v1/index.js index 8148d6a7a883..608ec38670e6 100644 --- a/x-pack/plugins/xpack_main/server/routes/api/v1/index.js +++ b/x-pack/plugins/xpack_main/server/routes/api/v1/index.js @@ -6,4 +6,5 @@ export { xpackInfoRoute } from './xpack_info'; export { telemetryRoute } from './telemetry'; +export { featuresRoute } from './features'; export { settingsRoute } from './settings'; diff --git a/x-pack/plugins/xpack_main/types.ts b/x-pack/plugins/xpack_main/types.ts new file mode 100644 index 000000000000..61dd43970d00 --- /dev/null +++ b/x-pack/plugins/xpack_main/types.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { + Feature, + FeatureKibanaPrivileges, + uiCapabilitiesRegex, +} from './server/lib/feature_registry'; diff --git a/x-pack/plugins/xpack_main/xpack_main.d.ts b/x-pack/plugins/xpack_main/xpack_main.d.ts index 2dac0da0c392..8a2471352c5f 100644 --- a/x-pack/plugins/xpack_main/xpack_main.d.ts +++ b/x-pack/plugins/xpack_main/xpack_main.d.ts @@ -4,9 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Feature, FeatureWithAllOrReadPrivileges } from './server/lib/feature_registry'; import { XPackInfo, XPackInfoOptions } from './server/lib/xpack_info'; +export { XPackFeature } from './server/lib/xpack_info'; export interface XPackMainPlugin { info: XPackInfo; createXPackInfo(options: XPackInfoOptions): XPackInfo; + getFeatures(): Feature[]; + registerFeature(feature: FeatureWithAllOrReadPrivileges): void; } diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 25c9e4808eba..a5a17411b7c7 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -18,5 +18,8 @@ require('@kbn/test').runTestsCli([ require.resolve('../test/saved_object_api_integration/security_and_spaces/config'), require.resolve('../test/saved_object_api_integration/security_only/config'), require.resolve('../test/saved_object_api_integration/spaces_only/config'), + require.resolve('../test/ui_capabilities/security_and_spaces/config'), + require.resolve('../test/ui_capabilities/security_only/config'), + require.resolve('../test/ui_capabilities/spaces_only/config'), require.resolve('../test/upgrade_assistant_integration/config'), ]); diff --git a/x-pack/test/api_integration/apis/apm/feature_controls.ts b/x-pack/test/api_integration/apis/apm/feature_controls.ts new file mode 100644 index 000000000000..e96ac46b210b --- /dev/null +++ b/x-pack/test/api_integration/apis/apm/feature_controls.ts @@ -0,0 +1,338 @@ +/* + * 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 { SecurityService, SpacesService } from '../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function featureControlsTests({ getService }: KibanaFunctionalTestDefaultProviders) { + const supertest = getService('supertestWithoutAuth'); + const security: SecurityService = getService('security'); + const spaces: SpacesService = getService('spaces'); + const log = getService('log'); + + const start = encodeURIComponent(new Date(Date.now() - 10000).toISOString()); + const end = encodeURIComponent(new Date().toISOString()); + + const expect404 = (result: any) => { + expect(result.error).to.be(undefined); + expect(result.response).not.to.be(undefined); + expect(result.response).to.have.property('statusCode', 404); + }; + + const expect200 = (result: any) => { + expect(result.error).to.be(undefined); + expect(result.response).not.to.be(undefined); + expect(result.response).to.have.property('statusCode', 200); + }; + + const endpoints = [ + { + url: `/api/apm/services/foo/errors?start=${start}&end=${end}`, + expectForbidden: expect404, + expectResponse: expect200, + }, + { + url: `/api/apm/services/foo/errors/bar?start=${start}&end=${end}`, + expectForbidden: expect404, + expectResponse: expect200, + }, + { + url: `/api/apm/services/foo/errors/bar/distribution?start=${start}&end=${end}`, + expectForbidden: expect404, + expectResponse: (result: any) => { + expect(result.response).to.have.property('statusCode', 400); + expect(result.response.body).to.have.property( + 'message', + "Cannot read property 'distribution' of undefined" + ); + }, + }, + { + url: `/api/apm/services/foo/errors/distribution?start=${start}&end=${end}`, + expectForbidden: expect404, + expectResponse: (result: any) => { + expect(result.response).to.have.property('statusCode', 400); + expect(result.response.body).to.have.property( + 'message', + "Cannot read property 'distribution' of undefined" + ); + }, + }, + { + url: `/api/apm/services/foo/metrics/charts?start=${start}&end=${end}`, + expectForbidden: expect404, + expectResponse: (result: any) => { + expect(result.response).to.have.property('statusCode', 400); + expect(result.response.body).to.have.property( + 'message', + "Cannot destructure property `timeseriesData` of 'undefined' or 'null'." + ); + }, + }, + { + url: `/api/apm/services?start=${start}&end=${end}`, + expectForbidden: expect404, + expectResponse: expect200, + }, + { + url: `/api/apm/services/foo?start=${start}&end=${end}`, + expectForbidden: expect404, + expectResponse: expect200, + }, + { + url: `/api/apm/traces?start=${start}&end=${end}`, + expectForbidden: expect404, + expectResponse: expect200, + }, + { + url: `/api/apm/traces/foo?start=${start}&end=${end}`, + expectForbidden: expect404, + expectResponse: (result: any) => { + expect(result.response).to.have.property('statusCode', 400); + expect(result.response.body).to.have.property( + 'message', + "Cannot read property 'transactions' of undefined" + ); + }, + }, + { + url: `/api/apm/services/foo/transaction_groups/bar?start=${start}&end=${end}`, + expectForbidden: expect404, + expectResponse: expect200, + }, + { + url: `/api/apm/services/foo/transaction_groups/bar/charts?start=${start}&end=${end}`, + expectForbidden: expect404, + expectResponse: expect200, + }, + { + url: `/api/apm/services/foo/transaction_groups/charts?start=${start}&end=${end}`, + expectForbidden: expect404, + expectResponse: expect200, + }, + { + url: `/api/apm/services/foo/transaction_groups/bar/baz/charts?start=${start}&end=${end}`, + expectForbidden: expect404, + expectResponse: expect200, + }, + { + url: `/api/apm/services/foo/transaction_groups/bar/baz/distribution?start=${start}&end=${end}`, + expectForbidden: expect404, + expectResponse: (result: any) => { + expect(result.response).to.have.property('statusCode', 400); + expect(result.response.body).to.have.property( + 'message', + "Cannot read property 'stats' of undefined" + ); + }, + }, + ]; + + async function executeRequest( + endpoint: string, + username: string, + password: string, + spaceId?: string + ) { + const basePath = spaceId ? `/s/${spaceId}` : ''; + + return await supertest + .get(`${basePath}${endpoint}`) + .auth(username, password) + .set('kbn-xsrf', 'foo') + .then((response: any) => ({ error: undefined, response })) + .catch((error: any) => ({ error, response: undefined })); + } + + async function executeRequests( + username: string, + password: string, + spaceId: string, + expectation: 'forbidden' | 'response' + ) { + for (const endpoint of endpoints) { + log.debug(`hitting ${endpoint}`); + const result = await executeRequest(endpoint.url, username, password, spaceId); + if (expectation === 'forbidden') { + endpoint.expectForbidden(result); + } else { + endpoint.expectResponse(result); + } + } + } + + describe('feature controls', () => { + it(`APIs can't be accessed by apm-* read privileges role`, async () => { + const username = 'logstash_read'; + const roleName = 'logstash_read'; + const password = `${username}-password`; + try { + await security.role.create(roleName, { + elasticsearch: { + indices: [ + { + names: ['apm-*'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + }); + + await security.user.create(username, { + password, + roles: [roleName], + full_name: 'a kibana user', + }); + + await executeRequests(username, password, '', 'forbidden'); + } finally { + await security.role.delete(roleName); + await security.user.delete(username); + } + }); + + it('APIs can be accessed global all with apm-* read privileges role', async () => { + const username = 'global_all'; + const roleName = 'global_all'; + const password = `${username}-password`; + try { + await security.role.create(roleName, { + elasticsearch: { + indices: [ + { + names: ['apm-*'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + kibana: [ + { + base: ['all'], + spaces: ['*'], + }, + ], + }); + + await security.user.create(username, { + password, + roles: [roleName], + full_name: 'a kibana user', + }); + + await executeRequests(username, password, '', 'response'); + } finally { + await security.role.delete(roleName); + await security.user.delete(username); + } + }); + + // this could be any role which doesn't have access to the APM feature + it(`APIs can't be accessed by dashboard all with apm-* read privileges role`, async () => { + const username = 'dashboard_all'; + const roleName = 'dashboard_all'; + const password = `${username}-password`; + try { + await security.role.create(roleName, { + elasticsearch: { + indices: [ + { + names: ['apm-*'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + kibana: [ + { + feature: { + dashboard: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create(username, { + password, + roles: [roleName], + full_name: 'a kibana user', + }); + + await executeRequests(username, password, '', 'forbidden'); + } finally { + await security.role.delete(roleName); + await security.user.delete(username); + } + }); + + describe('spaces', () => { + // the following tests create a user_1 which has uptime read access to space_1 and dashboard all access to space_2 + const space1Id = 'space_1'; + const space2Id = 'space_2'; + + const roleName = 'user_1'; + const username = 'user_1'; + const password = 'user_1-password'; + + before(async () => { + await spaces.create({ + id: space1Id, + name: space1Id, + disabledFeatures: [], + }); + await spaces.create({ + id: space2Id, + name: space2Id, + disabledFeatures: [], + }); + await security.role.create(roleName, { + elasticsearch: { + indices: [ + { + names: ['apm-*'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + kibana: [ + { + feature: { + apm: ['read'], + }, + spaces: [space1Id], + }, + { + feature: { + dashboard: ['all'], + }, + spaces: [space2Id], + }, + ], + }); + await security.user.create(username, { + password, + roles: [roleName], + }); + }); + + after(async () => { + await spaces.delete(space1Id); + await spaces.delete(space2Id); + await security.role.delete(roleName); + await security.user.delete(username); + }); + + it('user_1 can access APIs in space_1', async () => { + await executeRequests(username, password, space1Id, 'response'); + }); + + it(`user_1 can't access APIs in space_2`, async () => { + await executeRequests(username, password, space2Id, 'forbidden'); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/apm/index.ts b/x-pack/test/api_integration/apis/apm/index.ts new file mode 100644 index 000000000000..b51432f10d1e --- /dev/null +++ b/x-pack/test/api_integration/apis/apm/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function apmApiIntegrationTests({ + loadTestFile, +}: KibanaFunctionalTestDefaultProviders) { + describe('APM', () => { + loadTestFile(require.resolve('./feature_controls')); + }); +} diff --git a/x-pack/test/api_integration/apis/console/feature_controls.ts b/x-pack/test/api_integration/apis/console/feature_controls.ts new file mode 100644 index 000000000000..615883d1fbbd --- /dev/null +++ b/x-pack/test/api_integration/apis/console/feature_controls.ts @@ -0,0 +1,218 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SecurityService, SpacesService } from '../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function securityTests({ getService }: KibanaFunctionalTestDefaultProviders) { + const supertest = getService('supertestWithoutAuth'); + const security: SecurityService = getService('security'); + const spaces: SpacesService = getService('spaces'); + + describe('/api/console/proxy', () => { + it('cannot be accessed by an anonymous user', async () => { + await supertest + .post(`/api/console/proxy?method=GET&path=${encodeURIComponent('/_cat')}`) + .set('kbn-xsrf', 'xxx') + .send() + .expect(401); + }); + + it('can be accessed by kibana_user role', async () => { + const username = 'kibana_user'; + const roleName = 'kibana_user'; + try { + const password = `${username}-password`; + + await security.user.create(username, { + password, + roles: [roleName], + full_name: 'a kibana user', + }); + + await supertest + .post(`/api/console/proxy?method=GET&path=${encodeURIComponent('/_cat')}`) + .auth(username, password) + .set('kbn-xsrf', 'xxx') + .send() + .expect(200); + } finally { + await security.user.delete(username); + } + }); + + it('can be accessed by global all role', async () => { + const username = 'global_all'; + const roleName = 'global_all'; + try { + const password = `${username}-password`; + + await security.role.create(roleName, { + kibana: [ + { + base: ['all'], + spaces: ['*'], + }, + ], + }); + + await security.user.create(username, { + password, + roles: [roleName], + }); + + await supertest + .post(`/api/console/proxy?method=GET&path=${encodeURIComponent('/_cat')}`) + .auth(username, password) + .set('kbn-xsrf', 'xxx') + .send() + .expect(200); + } finally { + await security.role.delete(roleName); + await security.user.delete(username); + } + }); + + it('can be accessed by global read role', async () => { + const username = 'global_read'; + const roleName = 'global_read'; + try { + const password = `${username}-password`; + + await security.role.create(roleName, { + kibana: [ + { + base: ['read'], + spaces: ['*'], + }, + ], + }); + + await security.user.create(username, { + password, + roles: [roleName], + }); + + await supertest + .post(`/api/console/proxy?method=GET&path=${encodeURIComponent('/_cat')}`) + .auth(username, password) + .set('kbn-xsrf', 'xxx') + .send() + .expect(200); + } finally { + await security.role.delete(roleName); + await security.user.delete(username); + } + }); + + // this could be any role which doesn't have access to the dev_tools feature + it(`can't be accessed by a user with dashboard all access`, async () => { + const username = 'dashboard_all'; + const roleName = 'dashboard_all'; + try { + const password = `${username}-password`; + + await security.role.create(roleName, { + kibana: [ + { + feature: { + dashboard: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create(username, { + password, + roles: [roleName], + }); + + await supertest + .post(`/api/console/proxy?method=GET&path=${encodeURIComponent('/_cat')}`) + .auth(username, password) + .set('kbn-xsrf', 'xxx') + .send() + .expect(404); + } finally { + await security.role.delete(roleName); + await security.user.delete(username); + } + }); + + describe('spaces', () => { + // the following tests create a user_1 which has dev_tools all access to space_1 and dashboard access to space_2 + const space1Id = 'space_1'; + const user1 = { + username: 'user_1', + roleName: 'user_1', + password: 'user_1-password', + }; + + const space2Id = 'space_2'; + + before(async () => { + await spaces.create({ + id: space1Id, + name: space1Id, + disabledFeatures: [], + }); + await security.role.create(user1.roleName, { + kibana: [ + { + feature: { + dev_tools: ['all'], + }, + spaces: [space1Id], + }, + { + feature: { + dashboard: ['all'], + }, + spaces: [space2Id], + }, + ], + }); + await security.user.create(user1.username, { + password: user1.password, + roles: [user1.roleName], + }); + + await spaces.create({ + id: space2Id, + name: space2Id, + disabledFeatures: [], + }); + }); + + after(async () => { + await spaces.delete(space1Id); + await spaces.delete(space2Id); + await security.role.delete(user1.roleName); + await security.user.delete(user1.username); + }); + + it('user_1 can access dev_tools in space_1', async () => { + await supertest + .post(`/s/${space1Id}/api/console/proxy?method=GET&path=${encodeURIComponent('/_cat')}`) + .auth(user1.username, user1.password) + .set('kbn-xsrf', 'xxx') + .send() + .expect(200); + }); + + it(`user_1 can't access dev_tools in space_2`, async () => { + await supertest + .post(`/s/${space2Id}/api/console/proxy?method=GET&path=${encodeURIComponent('/_cat')}`) + .auth(user1.username, user1.password) + .set('kbn-xsrf', 'xxx') + .send() + .expect(404); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/console/index.ts b/x-pack/test/api_integration/apis/console/index.ts new file mode 100644 index 000000000000..512852a367be --- /dev/null +++ b/x-pack/test/api_integration/apis/console/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function consoleApiIntegrationTests({ + loadTestFile, +}: KibanaFunctionalTestDefaultProviders) { + describe('console', () => { + loadTestFile(require.resolve('./feature_controls')); + }); +} diff --git a/x-pack/test/api_integration/apis/index.js b/x-pack/test/api_integration/apis/index.js index 02bb37e130f2..5c33215392a3 100644 --- a/x-pack/test/api_integration/apis/index.js +++ b/x-pack/test/api_integration/apis/index.js @@ -16,8 +16,10 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./kibana')); loadTestFile(require.resolve('./infra')); loadTestFile(require.resolve('./beats')); + loadTestFile(require.resolve('./console')); loadTestFile(require.resolve('./management')); loadTestFile(require.resolve('./uptime')); loadTestFile(require.resolve('./maps')); + loadTestFile(require.resolve('./apm')); }); } diff --git a/x-pack/test/api_integration/apis/infra/feature_controls.ts b/x-pack/test/api_integration/apis/infra/feature_controls.ts new file mode 100644 index 000000000000..4f4899e86a01 --- /dev/null +++ b/x-pack/test/api_integration/apis/infra/feature_controls.ts @@ -0,0 +1,297 @@ +/* + * 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 gql from 'graphql-tag'; +import { SecurityService, SpacesService } from '../../../common/services'; +import { KbnTestProvider } from './types'; + +const introspectionQuery = gql` + query Schema { + __schema { + queryType { + name + } + } + } +`; + +const featureControlsTests: KbnTestProvider = ({ getService }) => { + const supertest = getService('supertestWithoutAuth'); + const security: SecurityService = getService('security'); + const spaces: SpacesService = getService('spaces'); + const clientFactory = getService('infraOpsGraphQLClientFactory'); + + const expectGraphQL404 = (result: any) => { + expect(result.response).to.be(undefined); + expect(result.error).not.to.be(undefined); + expect(result.error).to.have.property('networkError'); + expect(result.error.networkError).to.have.property('statusCode', 404); + }; + + const expectGraphQLResponse = (result: any) => { + expect(result.error).to.be(undefined); + expect(result.response).to.have.property('data'); + expect(result.response.data).to.be.an('object'); + }; + + const expectGraphIQL404 = (result: any) => { + expect(result.error).to.be(undefined); + expect(result.response).not.to.be(undefined); + expect(result.response).to.have.property('statusCode', 404); + }; + + const expectGraphIQLResponse = (result: any) => { + expect(result.error).to.be(undefined); + expect(result.response).not.to.be(undefined); + expect(result.response).to.have.property('statusCode', 200); + }; + + const executeGraphQLQuery = async (username: string, password: string, spaceId?: string) => { + const queryOptions = { + query: introspectionQuery, + }; + + const basePath = spaceId ? `/s/${spaceId}` : ''; + + const client = clientFactory({ username, password, basePath }); + let error; + let response; + try { + response = await client.query(queryOptions); + } catch (err) { + error = err; + } + return { + error, + response, + }; + }; + + const executeGraphIQLRequest = async (username: string, password: string, spaceId?: string) => { + const basePath = spaceId ? `/s/${spaceId}` : ''; + + return supertest + .get(`${basePath}/api/infra/graphql/graphiql`) + .auth(username, password) + .then((response: any) => ({ error: undefined, response })) + .catch((error: any) => ({ error, response: undefined })); + }; + + describe('feature controls', () => { + it(`APIs can't be accessed by user with logstash-* "read" privileges`, async () => { + const username = 'logstash_read'; + const roleName = 'logstash_read'; + const password = `${username}-password`; + try { + await security.role.create(roleName, { + elasticsearch: { + indices: [ + { + names: ['logstash-*'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + }); + + await security.user.create(username, { + password, + roles: [roleName], + full_name: 'a kibana user', + }); + + const graphQLResult = await executeGraphQLQuery(username, password); + expectGraphQL404(graphQLResult); + + const graphQLIResult = await executeGraphIQLRequest(username, password); + expectGraphIQL404(graphQLIResult); + } finally { + await security.role.delete(roleName); + await security.user.delete(username); + } + }); + + it('APIs can be accessed user with global "all" and logstash-* "read" privileges', async () => { + const username = 'global_all'; + const roleName = 'global_all'; + const password = `${username}-password`; + try { + await security.role.create(roleName, { + elasticsearch: { + indices: [ + { + names: ['logstash-*'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + kibana: [ + { + base: ['all'], + spaces: ['*'], + }, + ], + }); + + await security.user.create(username, { + password, + roles: [roleName], + full_name: 'a kibana user', + }); + + const graphQLResult = await executeGraphQLQuery(username, password); + expectGraphQLResponse(graphQLResult); + + const graphQLIResult = await executeGraphIQLRequest(username, password); + expectGraphIQLResponse(graphQLIResult); + } finally { + await security.role.delete(roleName); + await security.user.delete(username); + } + }); + + // this could be any role which doesn't have access to the infra feature + it(`APIs can't be accessed by user with dashboard "all" and logstash-* "read" privileges`, async () => { + const username = 'dashboard_all'; + const roleName = 'dashboard_all'; + const password = `${username}-password`; + try { + await security.role.create(roleName, { + elasticsearch: { + indices: [ + { + names: ['logstash-*'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + kibana: [ + { + feature: { + dashboard: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create(username, { + password, + roles: [roleName], + full_name: 'a kibana user', + }); + + const graphQLResult = await executeGraphQLQuery(username, password); + expectGraphQL404(graphQLResult); + + const graphQLIResult = await executeGraphIQLRequest(username, password); + expectGraphIQL404(graphQLIResult); + } finally { + await security.role.delete(roleName); + await security.user.delete(username); + } + }); + + describe('spaces', () => { + // the following tests create a user_1 which has infrastructure read access to space_1, logs read access to space_2 and dashboard all access to space_3 + const space1Id = 'space_1'; + const space2Id = 'space_2'; + const space3Id = 'space_3'; + + const roleName = 'user_1'; + const username = 'user_1'; + const password = 'user_1-password'; + + before(async () => { + await spaces.create({ + id: space1Id, + name: space1Id, + disabledFeatures: [], + }); + await spaces.create({ + id: space2Id, + name: space2Id, + disabledFeatures: [], + }); + await spaces.create({ + id: space3Id, + name: space3Id, + disabledFeatures: [], + }); + await security.role.create(roleName, { + elasticsearch: { + indices: [ + { + names: ['logstash-*'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + kibana: [ + { + feature: { + infrastructure: ['read'], + }, + spaces: [space1Id], + }, + { + feature: { + logs: ['read'], + }, + spaces: [space2Id], + }, + { + feature: { + dashboard: ['all'], + }, + spaces: [space3Id], + }, + ], + }); + await security.user.create(username, { + password, + roles: [roleName], + }); + }); + + after(async () => { + await spaces.delete(space1Id); + await spaces.delete(space2Id); + await spaces.delete(space3Id); + await security.role.delete(roleName); + await security.user.delete(username); + }); + + it('user_1 can access APIs in space_1', async () => { + const graphQLResult = await executeGraphQLQuery(username, password, space1Id); + expectGraphQLResponse(graphQLResult); + + const graphQLIResult = await executeGraphIQLRequest(username, password, space1Id); + expectGraphIQLResponse(graphQLIResult); + }); + + it(`user_1 can access APIs in space_2`, async () => { + const graphQLResult = await executeGraphQLQuery(username, password, space2Id); + expectGraphQLResponse(graphQLResult); + + const graphQLIResult = await executeGraphIQLRequest(username, password, space2Id); + expectGraphIQLResponse(graphQLIResult); + }); + + it(`user_1 can't access APIs in space_3`, async () => { + const graphQLResult = await executeGraphQLQuery(username, password, space3Id); + expectGraphQL404(graphQLResult); + + const graphQLIResult = await executeGraphIQLRequest(username, password, space3Id); + expectGraphIQL404(graphQLIResult); + }); + }); + }); +}; + +// eslint-disable-next-line import/no-default-export +export default featureControlsTests; diff --git a/x-pack/test/api_integration/apis/infra/index.js b/x-pack/test/api_integration/apis/infra/index.js index a4c2faed5f5c..c959facced06 100644 --- a/x-pack/test/api_integration/apis/infra/index.js +++ b/x-pack/test/api_integration/apis/infra/index.js @@ -14,5 +14,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./sources')); loadTestFile(require.resolve('./waffle')); loadTestFile(require.resolve('./log_item')); + loadTestFile(require.resolve('./feature_controls')); }); } diff --git a/x-pack/test/api_integration/apis/infra/types.ts b/x-pack/test/api_integration/apis/infra/types.ts index 932bef595269..585f10117b6c 100644 --- a/x-pack/test/api_integration/apis/infra/types.ts +++ b/x-pack/test/api_integration/apis/infra/types.ts @@ -12,10 +12,19 @@ export interface EsArchiver { unload(name: string): void; } +interface InfraOpsGraphQLClientFactoryOptions { + username: string; + password: string; + basePath: string; +} + export interface KbnTestProviderOptions { getService(name: string): any; getService(name: 'esArchiver'): EsArchiver; getService(name: 'infraOpsGraphQLClient'): ApolloClient; + getService( + name: 'infraOpsGraphQLClientFactory' + ): (options: InfraOpsGraphQLClientFactoryOptions) => ApolloClient; } export type KbnTestProvider = (options: KbnTestProviderOptions) => void; diff --git a/x-pack/test/api_integration/apis/kibana/stats/stats.js b/x-pack/test/api_integration/apis/kibana/stats/stats.js index 48c720bb57ba..cd0cae4ed105 100644 --- a/x-pack/test/api_integration/apis/kibana/stats/stats.js +++ b/x-pack/test/api_integration/apis/kibana/stats/stats.js @@ -9,17 +9,9 @@ import expect from '@kbn/expect'; export default function ({ getService }) { const supertestNoAuth = getService('supertestWithoutAuth'); const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); describe('/api/stats', () => { describe('operational stats and usage stats', () => { - before('load clusters archive', () => { - return esArchiver.load('discover'); - }); - - after('unload clusters archive', () => { - return esArchiver.unload('discover'); - }); describe('no auth', () => { it('should return 200 and stats for no extended', async () => { diff --git a/x-pack/test/api_integration/apis/security/index.js b/x-pack/test/api_integration/apis/security/index.js index ff3e5b33e832..571e6d9390ab 100644 --- a/x-pack/test/api_integration/apis/security/index.js +++ b/x-pack/test/api_integration/apis/security/index.js @@ -8,5 +8,6 @@ export default function ({ loadTestFile }) { describe('security', () => { loadTestFile(require.resolve('./basic_login')); loadTestFile(require.resolve('./roles')); + loadTestFile(require.resolve('./privileges')); }); } diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts new file mode 100644 index 000000000000..6da0748e607d --- /dev/null +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -0,0 +1,1341 @@ +/* + * 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 { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getService }: KibanaFunctionalTestDefaultProviders) { + const supertest = getService('supertest'); + + let version: string; + + describe('Privileges', () => { + before(async () => { + const versionService = getService('kibanaServer').version; + version = await versionService.get(); + }); + + // This test also functions as a sanity check for assigned privilege actions, to ensure that feature privileges are being granted in the way that developers expect. + // It could also be considered a huge maintenance burden, which will be largely alleviated once we can use jest's snapshotting abiliites + // For any poor soul which would prefer to just regenerate the following expected payload and look at the diff to ensure the proper changes + // were made, you can use the following bash command: + // + // curl -s -u elastic:changeme http://localhost:5620/api/security/privileges?includeActions=true 2>&1 | perl -pe 's/"(\w+:)(\d\.\d\.\d)([^"]*)"/`$1\${version}$3`/g' + // + describe('GET /api/security/privileges?includeActions=true', () => { + it('should return a privilege map with all known privileges with actions', async () => { + await supertest + .get('/api/security/privileges?includeActions=true') + .set('kbn-xsrf', 'xxx') + .send() + .expect(200, { + features: { + discover: { + all: [ + 'login:', + `version:${version}`, + `app:${version}:kibana`, + `ui:${version}:catalogue/discover`, + `ui:${version}:navLinks/kibana:discover`, + `saved_object:${version}:search/bulk_get`, + `saved_object:${version}:search/get`, + `saved_object:${version}:search/find`, + `saved_object:${version}:search/create`, + `saved_object:${version}:search/bulk_create`, + `saved_object:${version}:search/update`, + `saved_object:${version}:search/delete`, + `saved_object:${version}:url/bulk_get`, + `saved_object:${version}:url/get`, + `saved_object:${version}:url/find`, + `saved_object:${version}:url/create`, + `saved_object:${version}:url/bulk_create`, + `saved_object:${version}:url/update`, + `saved_object:${version}:url/delete`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:index-pattern/bulk_get`, + `saved_object:${version}:index-pattern/get`, + `saved_object:${version}:index-pattern/find`, + `ui:${version}:savedObjectsManagement/search/delete`, + `ui:${version}:savedObjectsManagement/search/edit`, + `ui:${version}:savedObjectsManagement/search/read`, + `ui:${version}:savedObjectsManagement/url/delete`, + `ui:${version}:savedObjectsManagement/url/edit`, + `ui:${version}:savedObjectsManagement/url/read`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:savedObjectsManagement/index-pattern/read`, + `ui:${version}:discover/show`, + `ui:${version}:discover/createShortUrl`, + `ui:${version}:discover/save`, + 'allHack:', + ], + read: [ + 'login:', + `version:${version}`, + `app:${version}:kibana`, + `ui:${version}:catalogue/discover`, + `ui:${version}:navLinks/kibana:discover`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:index-pattern/bulk_get`, + `saved_object:${version}:index-pattern/get`, + `saved_object:${version}:index-pattern/find`, + `saved_object:${version}:search/bulk_get`, + `saved_object:${version}:search/get`, + `saved_object:${version}:search/find`, + `saved_object:${version}:url/bulk_get`, + `saved_object:${version}:url/get`, + `saved_object:${version}:url/find`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:savedObjectsManagement/index-pattern/read`, + `ui:${version}:savedObjectsManagement/search/read`, + `ui:${version}:savedObjectsManagement/url/read`, + `ui:${version}:discover/show`, + ], + }, + visualize: { + all: [ + 'login:', + `version:${version}`, + `app:${version}:kibana`, + `ui:${version}:catalogue/visualize`, + `ui:${version}:navLinks/kibana:visualize`, + `saved_object:${version}:visualization/bulk_get`, + `saved_object:${version}:visualization/get`, + `saved_object:${version}:visualization/find`, + `saved_object:${version}:visualization/create`, + `saved_object:${version}:visualization/bulk_create`, + `saved_object:${version}:visualization/update`, + `saved_object:${version}:visualization/delete`, + `saved_object:${version}:url/bulk_get`, + `saved_object:${version}:url/get`, + `saved_object:${version}:url/find`, + `saved_object:${version}:url/create`, + `saved_object:${version}:url/bulk_create`, + `saved_object:${version}:url/update`, + `saved_object:${version}:url/delete`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:index-pattern/bulk_get`, + `saved_object:${version}:index-pattern/get`, + `saved_object:${version}:index-pattern/find`, + `saved_object:${version}:search/bulk_get`, + `saved_object:${version}:search/get`, + `saved_object:${version}:search/find`, + `ui:${version}:savedObjectsManagement/visualization/delete`, + `ui:${version}:savedObjectsManagement/visualization/edit`, + `ui:${version}:savedObjectsManagement/visualization/read`, + `ui:${version}:savedObjectsManagement/url/delete`, + `ui:${version}:savedObjectsManagement/url/edit`, + `ui:${version}:savedObjectsManagement/url/read`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:savedObjectsManagement/index-pattern/read`, + `ui:${version}:savedObjectsManagement/search/read`, + `ui:${version}:visualize/show`, + `ui:${version}:visualize/createShortUrl`, + `ui:${version}:visualize/delete`, + `ui:${version}:visualize/save`, + 'allHack:', + ], + read: [ + 'login:', + `version:${version}`, + `app:${version}:kibana`, + `ui:${version}:catalogue/visualize`, + `ui:${version}:navLinks/kibana:visualize`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:index-pattern/bulk_get`, + `saved_object:${version}:index-pattern/get`, + `saved_object:${version}:index-pattern/find`, + `saved_object:${version}:search/bulk_get`, + `saved_object:${version}:search/get`, + `saved_object:${version}:search/find`, + `saved_object:${version}:visualization/bulk_get`, + `saved_object:${version}:visualization/get`, + `saved_object:${version}:visualization/find`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:savedObjectsManagement/index-pattern/read`, + `ui:${version}:savedObjectsManagement/search/read`, + `ui:${version}:savedObjectsManagement/visualization/read`, + `ui:${version}:visualize/show`, + ], + }, + dashboard: { + all: [ + 'login:', + `version:${version}`, + `app:${version}:kibana`, + `ui:${version}:catalogue/dashboard`, + `ui:${version}:navLinks/kibana:dashboard`, + `saved_object:${version}:dashboard/bulk_get`, + `saved_object:${version}:dashboard/get`, + `saved_object:${version}:dashboard/find`, + `saved_object:${version}:dashboard/create`, + `saved_object:${version}:dashboard/bulk_create`, + `saved_object:${version}:dashboard/update`, + `saved_object:${version}:dashboard/delete`, + `saved_object:${version}:url/bulk_get`, + `saved_object:${version}:url/get`, + `saved_object:${version}:url/find`, + `saved_object:${version}:url/create`, + `saved_object:${version}:url/bulk_create`, + `saved_object:${version}:url/update`, + `saved_object:${version}:url/delete`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:index-pattern/bulk_get`, + `saved_object:${version}:index-pattern/get`, + `saved_object:${version}:index-pattern/find`, + `saved_object:${version}:search/bulk_get`, + `saved_object:${version}:search/get`, + `saved_object:${version}:search/find`, + `saved_object:${version}:visualization/bulk_get`, + `saved_object:${version}:visualization/get`, + `saved_object:${version}:visualization/find`, + `saved_object:${version}:timelion-sheet/bulk_get`, + `saved_object:${version}:timelion-sheet/get`, + `saved_object:${version}:timelion-sheet/find`, + `saved_object:${version}:canvas-workpad/bulk_get`, + `saved_object:${version}:canvas-workpad/get`, + `saved_object:${version}:canvas-workpad/find`, + `ui:${version}:savedObjectsManagement/dashboard/delete`, + `ui:${version}:savedObjectsManagement/dashboard/edit`, + `ui:${version}:savedObjectsManagement/dashboard/read`, + `ui:${version}:savedObjectsManagement/url/delete`, + `ui:${version}:savedObjectsManagement/url/edit`, + `ui:${version}:savedObjectsManagement/url/read`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:savedObjectsManagement/index-pattern/read`, + `ui:${version}:savedObjectsManagement/search/read`, + `ui:${version}:savedObjectsManagement/visualization/read`, + `ui:${version}:savedObjectsManagement/timelion-sheet/read`, + `ui:${version}:savedObjectsManagement/canvas-workpad/read`, + `ui:${version}:dashboard/createNew`, + `ui:${version}:dashboard/show`, + `ui:${version}:dashboard/showWriteControls`, + 'allHack:', + ], + read: [ + 'login:', + `version:${version}`, + `app:${version}:kibana`, + `ui:${version}:catalogue/dashboard`, + `ui:${version}:navLinks/kibana:dashboard`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:index-pattern/bulk_get`, + `saved_object:${version}:index-pattern/get`, + `saved_object:${version}:index-pattern/find`, + `saved_object:${version}:search/bulk_get`, + `saved_object:${version}:search/get`, + `saved_object:${version}:search/find`, + `saved_object:${version}:visualization/bulk_get`, + `saved_object:${version}:visualization/get`, + `saved_object:${version}:visualization/find`, + `saved_object:${version}:timelion-sheet/bulk_get`, + `saved_object:${version}:timelion-sheet/get`, + `saved_object:${version}:timelion-sheet/find`, + `saved_object:${version}:canvas-workpad/bulk_get`, + `saved_object:${version}:canvas-workpad/get`, + `saved_object:${version}:canvas-workpad/find`, + `saved_object:${version}:dashboard/bulk_get`, + `saved_object:${version}:dashboard/get`, + `saved_object:${version}:dashboard/find`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:savedObjectsManagement/index-pattern/read`, + `ui:${version}:savedObjectsManagement/search/read`, + `ui:${version}:savedObjectsManagement/visualization/read`, + `ui:${version}:savedObjectsManagement/timelion-sheet/read`, + `ui:${version}:savedObjectsManagement/canvas-workpad/read`, + `ui:${version}:savedObjectsManagement/dashboard/read`, + `ui:${version}:dashboard/show`, + ], + }, + dev_tools: { + all: [ + 'login:', + `version:${version}`, + `api:${version}:console`, + `app:${version}:kibana`, + `ui:${version}:catalogue/console`, + `ui:${version}:catalogue/searchprofiler`, + `ui:${version}:catalogue/grokdebugger`, + `ui:${version}:navLinks/kibana:dev_tools`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:dev_tools/show`, + 'allHack:', + ], + read: [ + 'login:', + `version:${version}`, + `api:${version}:console`, + `app:${version}:kibana`, + `ui:${version}:catalogue/console`, + `ui:${version}:catalogue/searchprofiler`, + `ui:${version}:catalogue/grokdebugger`, + `ui:${version}:navLinks/kibana:dev_tools`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:dev_tools/show`, + ], + }, + advancedSettings: { + all: [ + 'login:', + `version:${version}`, + `app:${version}:kibana`, + `ui:${version}:catalogue/advanced_settings`, + `ui:${version}:management/kibana/settings`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:config/create`, + `saved_object:${version}:config/bulk_create`, + `saved_object:${version}:config/update`, + `saved_object:${version}:config/delete`, + `ui:${version}:savedObjectsManagement/config/delete`, + `ui:${version}:savedObjectsManagement/config/edit`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:advancedSettings/save`, + 'allHack:', + ], + read: [ + 'login:', + `version:${version}`, + `app:${version}:kibana`, + `ui:${version}:catalogue/advanced_settings`, + `ui:${version}:management/kibana/settings`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `ui:${version}:savedObjectsManagement/config/read`, + ], + }, + indexPatterns: { + all: [ + 'login:', + `version:${version}`, + `app:${version}:kibana`, + `ui:${version}:catalogue/index_patterns`, + `ui:${version}:management/kibana/index_patterns`, + `saved_object:${version}:index-pattern/bulk_get`, + `saved_object:${version}:index-pattern/get`, + `saved_object:${version}:index-pattern/find`, + `saved_object:${version}:index-pattern/create`, + `saved_object:${version}:index-pattern/bulk_create`, + `saved_object:${version}:index-pattern/update`, + `saved_object:${version}:index-pattern/delete`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `ui:${version}:savedObjectsManagement/index-pattern/delete`, + `ui:${version}:savedObjectsManagement/index-pattern/edit`, + `ui:${version}:savedObjectsManagement/index-pattern/read`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:indexPatterns/createNew`, + 'allHack:', + ], + read: [ + 'login:', + `version:${version}`, + `app:${version}:kibana`, + `ui:${version}:catalogue/index_patterns`, + `ui:${version}:management/kibana/index_patterns`, + `saved_object:${version}:index-pattern/bulk_get`, + `saved_object:${version}:index-pattern/get`, + `saved_object:${version}:index-pattern/find`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `ui:${version}:savedObjectsManagement/index-pattern/read`, + `ui:${version}:savedObjectsManagement/config/read`, + ], + }, + timelion: { + all: [ + 'login:', + `version:${version}`, + `app:${version}:timelion`, + `app:${version}:kibana`, + `ui:${version}:catalogue/timelion`, + `ui:${version}:navLinks/timelion`, + `saved_object:${version}:timelion-sheet/bulk_get`, + `saved_object:${version}:timelion-sheet/get`, + `saved_object:${version}:timelion-sheet/find`, + `saved_object:${version}:timelion-sheet/create`, + `saved_object:${version}:timelion-sheet/bulk_create`, + `saved_object:${version}:timelion-sheet/update`, + `saved_object:${version}:timelion-sheet/delete`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:index-pattern/bulk_get`, + `saved_object:${version}:index-pattern/get`, + `saved_object:${version}:index-pattern/find`, + `ui:${version}:savedObjectsManagement/timelion-sheet/delete`, + `ui:${version}:savedObjectsManagement/timelion-sheet/edit`, + `ui:${version}:savedObjectsManagement/timelion-sheet/read`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:savedObjectsManagement/index-pattern/read`, + `ui:${version}:timelion/save`, + 'allHack:', + ], + read: [ + 'login:', + `version:${version}`, + `app:${version}:timelion`, + `app:${version}:kibana`, + `ui:${version}:catalogue/timelion`, + `ui:${version}:navLinks/timelion`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:index-pattern/bulk_get`, + `saved_object:${version}:index-pattern/get`, + `saved_object:${version}:index-pattern/find`, + `saved_object:${version}:timelion-sheet/bulk_get`, + `saved_object:${version}:timelion-sheet/get`, + `saved_object:${version}:timelion-sheet/find`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:savedObjectsManagement/index-pattern/read`, + `ui:${version}:savedObjectsManagement/timelion-sheet/read`, + ], + }, + graph: { + all: [ + 'login:', + `version:${version}`, + `app:${version}:graph`, + `app:${version}:kibana`, + `ui:${version}:catalogue/graph`, + `ui:${version}:navLinks/graph`, + `saved_object:${version}:graph-workspace/bulk_get`, + `saved_object:${version}:graph-workspace/get`, + `saved_object:${version}:graph-workspace/find`, + `saved_object:${version}:graph-workspace/create`, + `saved_object:${version}:graph-workspace/bulk_create`, + `saved_object:${version}:graph-workspace/update`, + `saved_object:${version}:graph-workspace/delete`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:index-pattern/bulk_get`, + `saved_object:${version}:index-pattern/get`, + `saved_object:${version}:index-pattern/find`, + `ui:${version}:savedObjectsManagement/graph-workspace/delete`, + `ui:${version}:savedObjectsManagement/graph-workspace/edit`, + `ui:${version}:savedObjectsManagement/graph-workspace/read`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:savedObjectsManagement/index-pattern/read`, + `ui:${version}:graph/save`, + `ui:${version}:graph/delete`, + 'allHack:', + ], + read: [ + 'login:', + `version:${version}`, + `app:${version}:graph`, + `app:${version}:kibana`, + `ui:${version}:catalogue/graph`, + `ui:${version}:navLinks/graph`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:index-pattern/bulk_get`, + `saved_object:${version}:index-pattern/get`, + `saved_object:${version}:index-pattern/find`, + `saved_object:${version}:graph-workspace/bulk_get`, + `saved_object:${version}:graph-workspace/get`, + `saved_object:${version}:graph-workspace/find`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:savedObjectsManagement/index-pattern/read`, + `ui:${version}:savedObjectsManagement/graph-workspace/read`, + ], + }, + apm: { + all: [ + 'login:', + `version:${version}`, + `api:${version}:apm`, + `app:${version}:apm`, + `app:${version}:kibana`, + `ui:${version}:catalogue/apm`, + `ui:${version}:navLinks/apm`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:apm/show`, + 'allHack:', + ], + read: [ + 'login:', + `version:${version}`, + `api:${version}:apm`, + `app:${version}:apm`, + `app:${version}:kibana`, + `ui:${version}:catalogue/apm`, + `ui:${version}:navLinks/apm`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:apm/show`, + ], + }, + maps: { + all: [ + 'login:', + `version:${version}`, + `app:${version}:maps`, + `app:${version}:kibana`, + `ui:${version}:catalogue/maps`, + `ui:${version}:navLinks/maps`, + `saved_object:${version}:map/bulk_get`, + `saved_object:${version}:map/get`, + `saved_object:${version}:map/find`, + `saved_object:${version}:map/create`, + `saved_object:${version}:map/bulk_create`, + `saved_object:${version}:map/update`, + `saved_object:${version}:map/delete`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:index-pattern/bulk_get`, + `saved_object:${version}:index-pattern/get`, + `saved_object:${version}:index-pattern/find`, + `ui:${version}:savedObjectsManagement/map/delete`, + `ui:${version}:savedObjectsManagement/map/edit`, + `ui:${version}:savedObjectsManagement/map/read`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:savedObjectsManagement/index-pattern/read`, + `ui:${version}:maps/save`, + 'allHack:', + ], + read: [ + 'login:', + `version:${version}`, + `app:${version}:maps`, + `app:${version}:kibana`, + `ui:${version}:catalogue/maps`, + `ui:${version}:navLinks/maps`, + `saved_object:${version}:map/bulk_get`, + `saved_object:${version}:map/get`, + `saved_object:${version}:map/find`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:index-pattern/bulk_get`, + `saved_object:${version}:index-pattern/get`, + `saved_object:${version}:index-pattern/find`, + `ui:${version}:savedObjectsManagement/map/read`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:savedObjectsManagement/index-pattern/read`, + ], + }, + canvas: { + all: [ + 'login:', + `version:${version}`, + `app:${version}:canvas`, + `app:${version}:kibana`, + `ui:${version}:catalogue/canvas`, + `ui:${version}:navLinks/canvas`, + `saved_object:${version}:canvas-workpad/bulk_get`, + `saved_object:${version}:canvas-workpad/get`, + `saved_object:${version}:canvas-workpad/find`, + `saved_object:${version}:canvas-workpad/create`, + `saved_object:${version}:canvas-workpad/bulk_create`, + `saved_object:${version}:canvas-workpad/update`, + `saved_object:${version}:canvas-workpad/delete`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:index-pattern/bulk_get`, + `saved_object:${version}:index-pattern/get`, + `saved_object:${version}:index-pattern/find`, + `ui:${version}:savedObjectsManagement/canvas-workpad/delete`, + `ui:${version}:savedObjectsManagement/canvas-workpad/edit`, + `ui:${version}:savedObjectsManagement/canvas-workpad/read`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:savedObjectsManagement/index-pattern/read`, + `ui:${version}:canvas/save`, + 'allHack:', + ], + read: [ + 'login:', + `version:${version}`, + `app:${version}:canvas`, + `app:${version}:kibana`, + `ui:${version}:catalogue/canvas`, + `ui:${version}:navLinks/canvas`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:index-pattern/bulk_get`, + `saved_object:${version}:index-pattern/get`, + `saved_object:${version}:index-pattern/find`, + `saved_object:${version}:canvas-workpad/bulk_get`, + `saved_object:${version}:canvas-workpad/get`, + `saved_object:${version}:canvas-workpad/find`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:savedObjectsManagement/index-pattern/read`, + `ui:${version}:savedObjectsManagement/canvas-workpad/read`, + ], + }, + infrastructure: { + all: [ + 'login:', + `version:${version}`, + `api:${version}:infra`, + `app:${version}:infra`, + `app:${version}:kibana`, + `ui:${version}:catalogue/infraops`, + `ui:${version}:navLinks/infra:home`, + `saved_object:${version}:infrastructure-ui-source/bulk_get`, + `saved_object:${version}:infrastructure-ui-source/get`, + `saved_object:${version}:infrastructure-ui-source/find`, + `saved_object:${version}:infrastructure-ui-source/create`, + `saved_object:${version}:infrastructure-ui-source/bulk_create`, + `saved_object:${version}:infrastructure-ui-source/update`, + `saved_object:${version}:infrastructure-ui-source/delete`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `ui:${version}:savedObjectsManagement/infrastructure-ui-source/delete`, + `ui:${version}:savedObjectsManagement/infrastructure-ui-source/edit`, + `ui:${version}:savedObjectsManagement/infrastructure-ui-source/read`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:infrastructure/show`, + `ui:${version}:infrastructure/configureSource`, + 'allHack:', + ], + read: [ + 'login:', + `version:${version}`, + `api:${version}:infra`, + `app:${version}:infra`, + `app:${version}:kibana`, + `ui:${version}:catalogue/infraops`, + `ui:${version}:navLinks/infra:home`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:infrastructure-ui-source/bulk_get`, + `saved_object:${version}:infrastructure-ui-source/get`, + `saved_object:${version}:infrastructure-ui-source/find`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:savedObjectsManagement/infrastructure-ui-source/read`, + `ui:${version}:infrastructure/show`, + ], + }, + logs: { + all: [ + 'login:', + `version:${version}`, + `api:${version}:infra`, + `app:${version}:infra`, + `app:${version}:kibana`, + `ui:${version}:catalogue/infralogging`, + `ui:${version}:navLinks/infra:logs`, + `saved_object:${version}:infrastructure-ui-source/bulk_get`, + `saved_object:${version}:infrastructure-ui-source/get`, + `saved_object:${version}:infrastructure-ui-source/find`, + `saved_object:${version}:infrastructure-ui-source/create`, + `saved_object:${version}:infrastructure-ui-source/bulk_create`, + `saved_object:${version}:infrastructure-ui-source/update`, + `saved_object:${version}:infrastructure-ui-source/delete`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `ui:${version}:savedObjectsManagement/infrastructure-ui-source/delete`, + `ui:${version}:savedObjectsManagement/infrastructure-ui-source/edit`, + `ui:${version}:savedObjectsManagement/infrastructure-ui-source/read`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:logs/show`, + `ui:${version}:logs/configureSource`, + 'allHack:', + ], + read: [ + 'login:', + `version:${version}`, + `api:${version}:infra`, + `app:${version}:infra`, + `app:${version}:kibana`, + `ui:${version}:catalogue/infralogging`, + `ui:${version}:navLinks/infra:logs`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:infrastructure-ui-source/bulk_get`, + `saved_object:${version}:infrastructure-ui-source/get`, + `saved_object:${version}:infrastructure-ui-source/find`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:savedObjectsManagement/infrastructure-ui-source/read`, + `ui:${version}:logs/show`, + ], + }, + uptime: { + all: [ + 'login:', + `version:${version}`, + `api:${version}:uptime`, + `app:${version}:uptime`, + `app:${version}:kibana`, + `ui:${version}:catalogue/uptime`, + `ui:${version}:navLinks/uptime`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `ui:${version}:savedObjectsManagement/config/read`, + 'allHack:', + ], + read: [ + 'login:', + `version:${version}`, + `api:${version}:uptime`, + `app:${version}:uptime`, + `app:${version}:kibana`, + `ui:${version}:catalogue/uptime`, + `ui:${version}:navLinks/uptime`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `ui:${version}:savedObjectsManagement/config/read`, + ], + }, + }, + global: { + all: [ + 'login:', + `version:${version}`, + `space:${version}:manage`, + `ui:${version}:spaces/manage`, + `app:${version}:kibana`, + `ui:${version}:catalogue/discover`, + `ui:${version}:navLinks/kibana:discover`, + `saved_object:${version}:search/bulk_get`, + `saved_object:${version}:search/get`, + `saved_object:${version}:search/find`, + `saved_object:${version}:search/create`, + `saved_object:${version}:search/bulk_create`, + `saved_object:${version}:search/update`, + `saved_object:${version}:search/delete`, + `saved_object:${version}:url/bulk_get`, + `saved_object:${version}:url/get`, + `saved_object:${version}:url/find`, + `saved_object:${version}:url/create`, + `saved_object:${version}:url/bulk_create`, + `saved_object:${version}:url/update`, + `saved_object:${version}:url/delete`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:index-pattern/bulk_get`, + `saved_object:${version}:index-pattern/get`, + `saved_object:${version}:index-pattern/find`, + `ui:${version}:savedObjectsManagement/search/delete`, + `ui:${version}:savedObjectsManagement/search/edit`, + `ui:${version}:savedObjectsManagement/search/read`, + `ui:${version}:savedObjectsManagement/url/delete`, + `ui:${version}:savedObjectsManagement/url/edit`, + `ui:${version}:savedObjectsManagement/url/read`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:savedObjectsManagement/index-pattern/read`, + `ui:${version}:discover/show`, + `ui:${version}:discover/createShortUrl`, + `ui:${version}:discover/save`, + `ui:${version}:catalogue/visualize`, + `ui:${version}:navLinks/kibana:visualize`, + `saved_object:${version}:visualization/bulk_get`, + `saved_object:${version}:visualization/get`, + `saved_object:${version}:visualization/find`, + `saved_object:${version}:visualization/create`, + `saved_object:${version}:visualization/bulk_create`, + `saved_object:${version}:visualization/update`, + `saved_object:${version}:visualization/delete`, + `ui:${version}:savedObjectsManagement/visualization/delete`, + `ui:${version}:savedObjectsManagement/visualization/edit`, + `ui:${version}:savedObjectsManagement/visualization/read`, + `ui:${version}:visualize/show`, + `ui:${version}:visualize/createShortUrl`, + `ui:${version}:visualize/delete`, + `ui:${version}:visualize/save`, + `ui:${version}:catalogue/dashboard`, + `ui:${version}:navLinks/kibana:dashboard`, + `saved_object:${version}:dashboard/bulk_get`, + `saved_object:${version}:dashboard/get`, + `saved_object:${version}:dashboard/find`, + `saved_object:${version}:dashboard/create`, + `saved_object:${version}:dashboard/bulk_create`, + `saved_object:${version}:dashboard/update`, + `saved_object:${version}:dashboard/delete`, + `saved_object:${version}:timelion-sheet/bulk_get`, + `saved_object:${version}:timelion-sheet/get`, + `saved_object:${version}:timelion-sheet/find`, + `saved_object:${version}:canvas-workpad/bulk_get`, + `saved_object:${version}:canvas-workpad/get`, + `saved_object:${version}:canvas-workpad/find`, + `ui:${version}:savedObjectsManagement/dashboard/delete`, + `ui:${version}:savedObjectsManagement/dashboard/edit`, + `ui:${version}:savedObjectsManagement/dashboard/read`, + `ui:${version}:savedObjectsManagement/timelion-sheet/read`, + `ui:${version}:savedObjectsManagement/canvas-workpad/read`, + `ui:${version}:dashboard/createNew`, + `ui:${version}:dashboard/show`, + `ui:${version}:dashboard/showWriteControls`, + `api:${version}:console`, + `ui:${version}:catalogue/console`, + `ui:${version}:catalogue/searchprofiler`, + `ui:${version}:catalogue/grokdebugger`, + `ui:${version}:navLinks/kibana:dev_tools`, + `ui:${version}:dev_tools/show`, + `ui:${version}:catalogue/advanced_settings`, + `ui:${version}:management/kibana/settings`, + `saved_object:${version}:config/create`, + `saved_object:${version}:config/bulk_create`, + `saved_object:${version}:config/update`, + `saved_object:${version}:config/delete`, + `ui:${version}:savedObjectsManagement/config/delete`, + `ui:${version}:savedObjectsManagement/config/edit`, + `ui:${version}:advancedSettings/save`, + `ui:${version}:catalogue/index_patterns`, + `ui:${version}:management/kibana/index_patterns`, + `saved_object:${version}:index-pattern/create`, + `saved_object:${version}:index-pattern/bulk_create`, + `saved_object:${version}:index-pattern/update`, + `saved_object:${version}:index-pattern/delete`, + `ui:${version}:savedObjectsManagement/index-pattern/delete`, + `ui:${version}:savedObjectsManagement/index-pattern/edit`, + `ui:${version}:indexPatterns/createNew`, + `app:${version}:timelion`, + `ui:${version}:catalogue/timelion`, + `ui:${version}:navLinks/timelion`, + `saved_object:${version}:timelion-sheet/create`, + `saved_object:${version}:timelion-sheet/bulk_create`, + `saved_object:${version}:timelion-sheet/update`, + `saved_object:${version}:timelion-sheet/delete`, + `ui:${version}:savedObjectsManagement/timelion-sheet/delete`, + `ui:${version}:savedObjectsManagement/timelion-sheet/edit`, + `ui:${version}:timelion/save`, + `app:${version}:graph`, + `ui:${version}:catalogue/graph`, + `ui:${version}:navLinks/graph`, + `saved_object:${version}:graph-workspace/bulk_get`, + `saved_object:${version}:graph-workspace/get`, + `saved_object:${version}:graph-workspace/find`, + `saved_object:${version}:graph-workspace/create`, + `saved_object:${version}:graph-workspace/bulk_create`, + `saved_object:${version}:graph-workspace/update`, + `saved_object:${version}:graph-workspace/delete`, + `ui:${version}:savedObjectsManagement/graph-workspace/delete`, + `ui:${version}:savedObjectsManagement/graph-workspace/edit`, + `ui:${version}:savedObjectsManagement/graph-workspace/read`, + `ui:${version}:graph/save`, + `ui:${version}:graph/delete`, + `api:${version}:apm`, + `app:${version}:apm`, + `ui:${version}:catalogue/apm`, + `ui:${version}:navLinks/apm`, + `ui:${version}:apm/show`, + `app:${version}:maps`, + `ui:${version}:catalogue/maps`, + `ui:${version}:navLinks/maps`, + `saved_object:${version}:map/bulk_get`, + `saved_object:${version}:map/get`, + `saved_object:${version}:map/find`, + `saved_object:${version}:map/create`, + `saved_object:${version}:map/bulk_create`, + `saved_object:${version}:map/update`, + `saved_object:${version}:map/delete`, + `ui:${version}:savedObjectsManagement/map/delete`, + `ui:${version}:savedObjectsManagement/map/edit`, + `ui:${version}:savedObjectsManagement/map/read`, + `ui:${version}:maps/save`, + `app:${version}:canvas`, + `ui:${version}:catalogue/canvas`, + `ui:${version}:navLinks/canvas`, + `saved_object:${version}:canvas-workpad/create`, + `saved_object:${version}:canvas-workpad/bulk_create`, + `saved_object:${version}:canvas-workpad/update`, + `saved_object:${version}:canvas-workpad/delete`, + `ui:${version}:savedObjectsManagement/canvas-workpad/delete`, + `ui:${version}:savedObjectsManagement/canvas-workpad/edit`, + `ui:${version}:canvas/save`, + `api:${version}:infra`, + `app:${version}:infra`, + `ui:${version}:catalogue/infraops`, + `ui:${version}:navLinks/infra:home`, + `saved_object:${version}:infrastructure-ui-source/bulk_get`, + `saved_object:${version}:infrastructure-ui-source/get`, + `saved_object:${version}:infrastructure-ui-source/find`, + `saved_object:${version}:infrastructure-ui-source/create`, + `saved_object:${version}:infrastructure-ui-source/bulk_create`, + `saved_object:${version}:infrastructure-ui-source/update`, + `saved_object:${version}:infrastructure-ui-source/delete`, + `ui:${version}:savedObjectsManagement/infrastructure-ui-source/delete`, + `ui:${version}:savedObjectsManagement/infrastructure-ui-source/edit`, + `ui:${version}:savedObjectsManagement/infrastructure-ui-source/read`, + `ui:${version}:infrastructure/show`, + `ui:${version}:infrastructure/configureSource`, + `ui:${version}:catalogue/infralogging`, + `ui:${version}:navLinks/infra:logs`, + `ui:${version}:logs/show`, + `ui:${version}:logs/configureSource`, + `api:${version}:uptime`, + `app:${version}:uptime`, + `ui:${version}:catalogue/uptime`, + `ui:${version}:navLinks/uptime`, + 'allHack:', + ], + read: [ + 'login:', + `version:${version}`, + `app:${version}:kibana`, + `ui:${version}:catalogue/discover`, + `ui:${version}:navLinks/kibana:discover`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:index-pattern/bulk_get`, + `saved_object:${version}:index-pattern/get`, + `saved_object:${version}:index-pattern/find`, + `saved_object:${version}:search/bulk_get`, + `saved_object:${version}:search/get`, + `saved_object:${version}:search/find`, + `saved_object:${version}:url/bulk_get`, + `saved_object:${version}:url/get`, + `saved_object:${version}:url/find`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:savedObjectsManagement/index-pattern/read`, + `ui:${version}:savedObjectsManagement/search/read`, + `ui:${version}:savedObjectsManagement/url/read`, + `ui:${version}:discover/show`, + `ui:${version}:catalogue/visualize`, + `ui:${version}:navLinks/kibana:visualize`, + `saved_object:${version}:visualization/bulk_get`, + `saved_object:${version}:visualization/get`, + `saved_object:${version}:visualization/find`, + `ui:${version}:savedObjectsManagement/visualization/read`, + `ui:${version}:visualize/show`, + `ui:${version}:catalogue/dashboard`, + `ui:${version}:navLinks/kibana:dashboard`, + `saved_object:${version}:timelion-sheet/bulk_get`, + `saved_object:${version}:timelion-sheet/get`, + `saved_object:${version}:timelion-sheet/find`, + `saved_object:${version}:canvas-workpad/bulk_get`, + `saved_object:${version}:canvas-workpad/get`, + `saved_object:${version}:canvas-workpad/find`, + `saved_object:${version}:dashboard/bulk_get`, + `saved_object:${version}:dashboard/get`, + `saved_object:${version}:dashboard/find`, + `ui:${version}:savedObjectsManagement/timelion-sheet/read`, + `ui:${version}:savedObjectsManagement/canvas-workpad/read`, + `ui:${version}:savedObjectsManagement/dashboard/read`, + `ui:${version}:dashboard/show`, + `api:${version}:console`, + `ui:${version}:catalogue/console`, + `ui:${version}:catalogue/searchprofiler`, + `ui:${version}:catalogue/grokdebugger`, + `ui:${version}:navLinks/kibana:dev_tools`, + `ui:${version}:dev_tools/show`, + `ui:${version}:catalogue/advanced_settings`, + `ui:${version}:management/kibana/settings`, + `ui:${version}:catalogue/index_patterns`, + `ui:${version}:management/kibana/index_patterns`, + `app:${version}:timelion`, + `ui:${version}:catalogue/timelion`, + `ui:${version}:navLinks/timelion`, + `app:${version}:graph`, + `ui:${version}:catalogue/graph`, + `ui:${version}:navLinks/graph`, + `saved_object:${version}:graph-workspace/bulk_get`, + `saved_object:${version}:graph-workspace/get`, + `saved_object:${version}:graph-workspace/find`, + `ui:${version}:savedObjectsManagement/graph-workspace/read`, + `api:${version}:apm`, + `app:${version}:apm`, + `ui:${version}:catalogue/apm`, + `ui:${version}:navLinks/apm`, + `ui:${version}:apm/show`, + `app:${version}:maps`, + `ui:${version}:catalogue/maps`, + `ui:${version}:navLinks/maps`, + `saved_object:${version}:map/bulk_get`, + `saved_object:${version}:map/get`, + `saved_object:${version}:map/find`, + `ui:${version}:savedObjectsManagement/map/read`, + `app:${version}:canvas`, + `ui:${version}:catalogue/canvas`, + `ui:${version}:navLinks/canvas`, + `api:${version}:infra`, + `app:${version}:infra`, + `ui:${version}:catalogue/infraops`, + `ui:${version}:navLinks/infra:home`, + `saved_object:${version}:infrastructure-ui-source/bulk_get`, + `saved_object:${version}:infrastructure-ui-source/get`, + `saved_object:${version}:infrastructure-ui-source/find`, + `ui:${version}:savedObjectsManagement/infrastructure-ui-source/read`, + `ui:${version}:infrastructure/show`, + `ui:${version}:catalogue/infralogging`, + `ui:${version}:navLinks/infra:logs`, + `ui:${version}:logs/show`, + `api:${version}:uptime`, + `app:${version}:uptime`, + `ui:${version}:catalogue/uptime`, + `ui:${version}:navLinks/uptime`, + ], + }, + space: { + all: [ + 'login:', + `version:${version}`, + `app:${version}:kibana`, + `ui:${version}:catalogue/discover`, + `ui:${version}:navLinks/kibana:discover`, + `saved_object:${version}:search/bulk_get`, + `saved_object:${version}:search/get`, + `saved_object:${version}:search/find`, + `saved_object:${version}:search/create`, + `saved_object:${version}:search/bulk_create`, + `saved_object:${version}:search/update`, + `saved_object:${version}:search/delete`, + `saved_object:${version}:url/bulk_get`, + `saved_object:${version}:url/get`, + `saved_object:${version}:url/find`, + `saved_object:${version}:url/create`, + `saved_object:${version}:url/bulk_create`, + `saved_object:${version}:url/update`, + `saved_object:${version}:url/delete`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:index-pattern/bulk_get`, + `saved_object:${version}:index-pattern/get`, + `saved_object:${version}:index-pattern/find`, + `ui:${version}:savedObjectsManagement/search/delete`, + `ui:${version}:savedObjectsManagement/search/edit`, + `ui:${version}:savedObjectsManagement/search/read`, + `ui:${version}:savedObjectsManagement/url/delete`, + `ui:${version}:savedObjectsManagement/url/edit`, + `ui:${version}:savedObjectsManagement/url/read`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:savedObjectsManagement/index-pattern/read`, + `ui:${version}:discover/show`, + `ui:${version}:discover/createShortUrl`, + `ui:${version}:discover/save`, + `ui:${version}:catalogue/visualize`, + `ui:${version}:navLinks/kibana:visualize`, + `saved_object:${version}:visualization/bulk_get`, + `saved_object:${version}:visualization/get`, + `saved_object:${version}:visualization/find`, + `saved_object:${version}:visualization/create`, + `saved_object:${version}:visualization/bulk_create`, + `saved_object:${version}:visualization/update`, + `saved_object:${version}:visualization/delete`, + `ui:${version}:savedObjectsManagement/visualization/delete`, + `ui:${version}:savedObjectsManagement/visualization/edit`, + `ui:${version}:savedObjectsManagement/visualization/read`, + `ui:${version}:visualize/show`, + `ui:${version}:visualize/createShortUrl`, + `ui:${version}:visualize/delete`, + `ui:${version}:visualize/save`, + `ui:${version}:catalogue/dashboard`, + `ui:${version}:navLinks/kibana:dashboard`, + `saved_object:${version}:dashboard/bulk_get`, + `saved_object:${version}:dashboard/get`, + `saved_object:${version}:dashboard/find`, + `saved_object:${version}:dashboard/create`, + `saved_object:${version}:dashboard/bulk_create`, + `saved_object:${version}:dashboard/update`, + `saved_object:${version}:dashboard/delete`, + `saved_object:${version}:timelion-sheet/bulk_get`, + `saved_object:${version}:timelion-sheet/get`, + `saved_object:${version}:timelion-sheet/find`, + `saved_object:${version}:canvas-workpad/bulk_get`, + `saved_object:${version}:canvas-workpad/get`, + `saved_object:${version}:canvas-workpad/find`, + `ui:${version}:savedObjectsManagement/dashboard/delete`, + `ui:${version}:savedObjectsManagement/dashboard/edit`, + `ui:${version}:savedObjectsManagement/dashboard/read`, + `ui:${version}:savedObjectsManagement/timelion-sheet/read`, + `ui:${version}:savedObjectsManagement/canvas-workpad/read`, + `ui:${version}:dashboard/createNew`, + `ui:${version}:dashboard/show`, + `ui:${version}:dashboard/showWriteControls`, + `api:${version}:console`, + `ui:${version}:catalogue/console`, + `ui:${version}:catalogue/searchprofiler`, + `ui:${version}:catalogue/grokdebugger`, + `ui:${version}:navLinks/kibana:dev_tools`, + `ui:${version}:dev_tools/show`, + `ui:${version}:catalogue/advanced_settings`, + `ui:${version}:management/kibana/settings`, + `saved_object:${version}:config/create`, + `saved_object:${version}:config/bulk_create`, + `saved_object:${version}:config/update`, + `saved_object:${version}:config/delete`, + `ui:${version}:savedObjectsManagement/config/delete`, + `ui:${version}:savedObjectsManagement/config/edit`, + `ui:${version}:advancedSettings/save`, + `ui:${version}:catalogue/index_patterns`, + `ui:${version}:management/kibana/index_patterns`, + `saved_object:${version}:index-pattern/create`, + `saved_object:${version}:index-pattern/bulk_create`, + `saved_object:${version}:index-pattern/update`, + `saved_object:${version}:index-pattern/delete`, + `ui:${version}:savedObjectsManagement/index-pattern/delete`, + `ui:${version}:savedObjectsManagement/index-pattern/edit`, + `ui:${version}:indexPatterns/createNew`, + `app:${version}:timelion`, + `ui:${version}:catalogue/timelion`, + `ui:${version}:navLinks/timelion`, + `saved_object:${version}:timelion-sheet/create`, + `saved_object:${version}:timelion-sheet/bulk_create`, + `saved_object:${version}:timelion-sheet/update`, + `saved_object:${version}:timelion-sheet/delete`, + `ui:${version}:savedObjectsManagement/timelion-sheet/delete`, + `ui:${version}:savedObjectsManagement/timelion-sheet/edit`, + `ui:${version}:timelion/save`, + `app:${version}:graph`, + `ui:${version}:catalogue/graph`, + `ui:${version}:navLinks/graph`, + `saved_object:${version}:graph-workspace/bulk_get`, + `saved_object:${version}:graph-workspace/get`, + `saved_object:${version}:graph-workspace/find`, + `saved_object:${version}:graph-workspace/create`, + `saved_object:${version}:graph-workspace/bulk_create`, + `saved_object:${version}:graph-workspace/update`, + `saved_object:${version}:graph-workspace/delete`, + `ui:${version}:savedObjectsManagement/graph-workspace/delete`, + `ui:${version}:savedObjectsManagement/graph-workspace/edit`, + `ui:${version}:savedObjectsManagement/graph-workspace/read`, + `ui:${version}:graph/save`, + `ui:${version}:graph/delete`, + `api:${version}:apm`, + `app:${version}:apm`, + `ui:${version}:catalogue/apm`, + `ui:${version}:navLinks/apm`, + `ui:${version}:apm/show`, + `app:${version}:maps`, + `ui:${version}:catalogue/maps`, + `ui:${version}:navLinks/maps`, + `saved_object:${version}:map/bulk_get`, + `saved_object:${version}:map/get`, + `saved_object:${version}:map/find`, + `saved_object:${version}:map/create`, + `saved_object:${version}:map/bulk_create`, + `saved_object:${version}:map/update`, + `saved_object:${version}:map/delete`, + `ui:${version}:savedObjectsManagement/map/delete`, + `ui:${version}:savedObjectsManagement/map/edit`, + `ui:${version}:savedObjectsManagement/map/read`, + `ui:${version}:maps/save`, + `app:${version}:canvas`, + `ui:${version}:catalogue/canvas`, + `ui:${version}:navLinks/canvas`, + `saved_object:${version}:canvas-workpad/create`, + `saved_object:${version}:canvas-workpad/bulk_create`, + `saved_object:${version}:canvas-workpad/update`, + `saved_object:${version}:canvas-workpad/delete`, + `ui:${version}:savedObjectsManagement/canvas-workpad/delete`, + `ui:${version}:savedObjectsManagement/canvas-workpad/edit`, + `ui:${version}:canvas/save`, + `api:${version}:infra`, + `app:${version}:infra`, + `ui:${version}:catalogue/infraops`, + `ui:${version}:navLinks/infra:home`, + `saved_object:${version}:infrastructure-ui-source/bulk_get`, + `saved_object:${version}:infrastructure-ui-source/get`, + `saved_object:${version}:infrastructure-ui-source/find`, + `saved_object:${version}:infrastructure-ui-source/create`, + `saved_object:${version}:infrastructure-ui-source/bulk_create`, + `saved_object:${version}:infrastructure-ui-source/update`, + `saved_object:${version}:infrastructure-ui-source/delete`, + `ui:${version}:savedObjectsManagement/infrastructure-ui-source/delete`, + `ui:${version}:savedObjectsManagement/infrastructure-ui-source/edit`, + `ui:${version}:savedObjectsManagement/infrastructure-ui-source/read`, + `ui:${version}:infrastructure/show`, + `ui:${version}:infrastructure/configureSource`, + `ui:${version}:catalogue/infralogging`, + `ui:${version}:navLinks/infra:logs`, + `ui:${version}:logs/show`, + `ui:${version}:logs/configureSource`, + `api:${version}:uptime`, + `app:${version}:uptime`, + `ui:${version}:catalogue/uptime`, + `ui:${version}:navLinks/uptime`, + 'allHack:', + ], + read: [ + 'login:', + `version:${version}`, + `app:${version}:kibana`, + `ui:${version}:catalogue/discover`, + `ui:${version}:navLinks/kibana:discover`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:index-pattern/bulk_get`, + `saved_object:${version}:index-pattern/get`, + `saved_object:${version}:index-pattern/find`, + `saved_object:${version}:search/bulk_get`, + `saved_object:${version}:search/get`, + `saved_object:${version}:search/find`, + `saved_object:${version}:url/bulk_get`, + `saved_object:${version}:url/get`, + `saved_object:${version}:url/find`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:savedObjectsManagement/index-pattern/read`, + `ui:${version}:savedObjectsManagement/search/read`, + `ui:${version}:savedObjectsManagement/url/read`, + `ui:${version}:discover/show`, + `ui:${version}:catalogue/visualize`, + `ui:${version}:navLinks/kibana:visualize`, + `saved_object:${version}:visualization/bulk_get`, + `saved_object:${version}:visualization/get`, + `saved_object:${version}:visualization/find`, + `ui:${version}:savedObjectsManagement/visualization/read`, + `ui:${version}:visualize/show`, + `ui:${version}:catalogue/dashboard`, + `ui:${version}:navLinks/kibana:dashboard`, + `saved_object:${version}:timelion-sheet/bulk_get`, + `saved_object:${version}:timelion-sheet/get`, + `saved_object:${version}:timelion-sheet/find`, + `saved_object:${version}:canvas-workpad/bulk_get`, + `saved_object:${version}:canvas-workpad/get`, + `saved_object:${version}:canvas-workpad/find`, + `saved_object:${version}:dashboard/bulk_get`, + `saved_object:${version}:dashboard/get`, + `saved_object:${version}:dashboard/find`, + `ui:${version}:savedObjectsManagement/timelion-sheet/read`, + `ui:${version}:savedObjectsManagement/canvas-workpad/read`, + `ui:${version}:savedObjectsManagement/dashboard/read`, + `ui:${version}:dashboard/show`, + `api:${version}:console`, + `ui:${version}:catalogue/console`, + `ui:${version}:catalogue/searchprofiler`, + `ui:${version}:catalogue/grokdebugger`, + `ui:${version}:navLinks/kibana:dev_tools`, + `ui:${version}:dev_tools/show`, + `ui:${version}:catalogue/advanced_settings`, + `ui:${version}:management/kibana/settings`, + `ui:${version}:catalogue/index_patterns`, + `ui:${version}:management/kibana/index_patterns`, + `app:${version}:timelion`, + `ui:${version}:catalogue/timelion`, + `ui:${version}:navLinks/timelion`, + `app:${version}:graph`, + `ui:${version}:catalogue/graph`, + `ui:${version}:navLinks/graph`, + `saved_object:${version}:graph-workspace/bulk_get`, + `saved_object:${version}:graph-workspace/get`, + `saved_object:${version}:graph-workspace/find`, + `ui:${version}:savedObjectsManagement/graph-workspace/read`, + `api:${version}:apm`, + `app:${version}:apm`, + `ui:${version}:catalogue/apm`, + `ui:${version}:navLinks/apm`, + `ui:${version}:apm/show`, + `app:${version}:maps`, + `ui:${version}:catalogue/maps`, + `ui:${version}:navLinks/maps`, + `saved_object:${version}:map/bulk_get`, + `saved_object:${version}:map/get`, + `saved_object:${version}:map/find`, + `ui:${version}:savedObjectsManagement/map/read`, + `app:${version}:canvas`, + `ui:${version}:catalogue/canvas`, + `ui:${version}:navLinks/canvas`, + `api:${version}:infra`, + `app:${version}:infra`, + `ui:${version}:catalogue/infraops`, + `ui:${version}:navLinks/infra:home`, + `saved_object:${version}:infrastructure-ui-source/bulk_get`, + `saved_object:${version}:infrastructure-ui-source/get`, + `saved_object:${version}:infrastructure-ui-source/find`, + `ui:${version}:savedObjectsManagement/infrastructure-ui-source/read`, + `ui:${version}:infrastructure/show`, + `ui:${version}:catalogue/infralogging`, + `ui:${version}:navLinks/infra:logs`, + `ui:${version}:logs/show`, + `api:${version}:uptime`, + `app:${version}:uptime`, + `ui:${version}:catalogue/uptime`, + `ui:${version}:navLinks/uptime`, + ], + }, + reserved: { + monitoring: [ + `version:${version}`, + `app:${version}:monitoring`, + `app:${version}:kibana`, + `ui:${version}:catalogue/monitoring`, + `ui:${version}:navLinks/monitoring`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `ui:${version}:savedObjectsManagement/config/read`, + ], + ml: [ + `version:${version}`, + `app:${version}:ml`, + `app:${version}:kibana`, + `ui:${version}:catalogue/ml`, + `ui:${version}:navLinks/ml`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `ui:${version}:savedObjectsManagement/config/read`, + ], + }, + }); + }); + + describe('GET /api/security/privileges', () => { + it('should return a privilege map with all known privileges, without actions', async () => { + await supertest + .get('/api/security/privileges') + .set('kbn-xsrf', 'xxx') + .send() + .expect(200, { + features: { + discover: ['all', 'read'], + visualize: ['all', 'read'], + dashboard: ['all', 'read'], + dev_tools: ['all', 'read'], + advancedSettings: ['all', 'read'], + indexPatterns: ['all', 'read'], + timelion: ['all', 'read'], + graph: ['all', 'read'], + maps: ['all', 'read'], + canvas: ['all', 'read'], + infrastructure: ['all', 'read'], + logs: ['all', 'read'], + uptime: ['all', 'read'], + apm: ['all', 'read'], + }, + global: ['all', 'read'], + space: ['all', 'read'], + reserved: ['monitoring', 'ml'], + }); + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/security/roles.js b/x-pack/test/api_integration/apis/security/roles.js index 46eb56ab5602..5e5c2f3d3b18 100644 --- a/x-pack/test/api_integration/apis/security/roles.js +++ b/x-pack/test/api_integration/apis/security/roles.js @@ -41,10 +41,24 @@ export default function ({ getService }) { ], run_as: ['watcher_user'], }, - kibana: { - global: ['all', 'read'], - space: {} - } + kibana: [ + { + base: ['read'], + feature: { + dashboard: ['read'], + dev_tools: ['all'], + } + }, + { + base: ['all'], + feature: { + dashboard: ['read'], + discover: ['all'], + ml: ['all'] + }, + spaces: ['marketing', 'sales'] + } + ] }) .expect(204); @@ -67,8 +81,13 @@ export default function ({ getService }) { applications: [ { application: 'kibana-.kibana', - privileges: ['all', 'read'], + privileges: ['read', 'feature_dashboard.read', 'feature_dev_tools.all'], resources: ['*'], + }, + { + application: 'kibana-.kibana', + privileges: ['space_all', 'feature_dashboard.read', 'feature_discover.all', 'feature_ml.all'], + resources: ['space:marketing', 'space:sales'], } ], run_as: ['watcher_user'], @@ -141,10 +160,25 @@ export default function ({ getService }) { ], run_as: ['watcher_user'], }, - kibana: { - global: ['all', 'read'], - space: {} - } + kibana: [ + { + base: ['read'], + feature: { + dashboard: ['read'], + dev_tools: ['all'], + }, + spaces: ['*'] + }, + { + base: ['all'], + feature: { + dashboard: ['read'], + discover: ['all'], + ml: ['all'] + }, + spaces: ['marketing', 'sales'] + } + ], }) .expect(204); @@ -167,9 +201,14 @@ export default function ({ getService }) { applications: [ { application: 'kibana-.kibana', - privileges: ['all', 'read'], + privileges: ['read', 'feature_dashboard.read', 'feature_dev_tools.all'], resources: ['*'], }, + { + application: 'kibana-.kibana', + privileges: ['space_all', 'feature_dashboard.read', 'feature_discover.all', 'feature_ml.all'], + resources: ['space:marketing', 'space:sales'], + }, { application: 'logstash-default', privileges: ['logstash-privilege'], @@ -188,6 +227,100 @@ export default function ({ getService }) { }); }); + describe('Get Role', async () => { + it('should get roles', async () => { + await es.shield.putRole({ + name: 'role_to_get', + body: { + cluster: ['manage'], + indices: [ + { + names: ['logstash-*'], + privileges: ['read', 'view_index_metadata'], + allow_restricted_indices: false, + field_security: { + grant: ['*'], + except: ['geo.*'] + }, + query: `{ "match": { "geo.src": "CN" } }`, + }, + ], + applications: [ + { + application: 'kibana-.kibana', + privileges: ['read', 'feature_dashboard.read', 'feature_dev_tools.all'], + resources: ['*'], + }, + { + application: 'kibana-.kibana', + privileges: ['space_all', 'feature_dashboard.read', 'feature_discover.all', 'feature_ml.all'], + resources: ['space:marketing', 'space:sales'], + }, + { + application: 'logstash-default', + privileges: ['logstash-privilege'], + resources: ['*'], + }, + ], + run_as: ['watcher_user'], + metadata: { + foo: 'test-metadata', + }, + transient_metadata: { + enabled: true, + }, + } + }); + + await supertest.get('/api/security/role/role_to_get') + .set('kbn-xsrf', 'xxx') + .expect(200, { + name: 'role_to_get', + metadata: { + foo: 'test-metadata', + }, + transient_metadata: { enabled: true }, + elasticsearch: { + cluster: ['manage'], + indices: [ + { + field_security: { + grant: ['*'], + except: ['geo.*'] + }, + names: ['logstash-*'], + privileges: ['read', 'view_index_metadata'], + query: `{ "match": { "geo.src": "CN" } }`, + allow_restricted_indices: false + }, + ], + run_as: ['watcher_user'], + }, + kibana: [ + { + base: ['read'], + feature: { + dashboard: ['read'], + dev_tools: ['all'], + }, + spaces: ['*'] + }, + { + base: ['all'], + feature: { + dashboard: ['read'], + discover: ['all'], + ml: ['all'] + }, + spaces: ['marketing', 'sales'] + } + ], + + _transform_error: [], + _unrecognized_applications: [ 'logstash-default' ] + }); + }); + }); describe('Delete Role', () => { it('should delete the three roles we created', async () => { await supertest.delete('/api/security/role/empty_role').set('kbn-xsrf', 'xxx').expect(204); diff --git a/x-pack/test/api_integration/apis/uptime/constants.ts b/x-pack/test/api_integration/apis/uptime/constants.ts new file mode 100644 index 000000000000..664a2a467df7 --- /dev/null +++ b/x-pack/test/api_integration/apis/uptime/constants.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. + */ + +import moment from 'moment'; + +export const PINGS_DATE_RANGE_START = moment('2018-10-30T00:00:23.889Z').valueOf(); +export const PINGS_DATE_RANGE_END = moment('2018-10-31T00:00:00.889Z').valueOf(); diff --git a/x-pack/test/api_integration/apis/uptime/feature_controls.ts b/x-pack/test/api_integration/apis/uptime/feature_controls.ts new file mode 100644 index 000000000000..243e5ebdddd9 --- /dev/null +++ b/x-pack/test/api_integration/apis/uptime/feature_controls.ts @@ -0,0 +1,276 @@ +/* + * 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 { docCountQueryString } from '../../../../plugins/uptime/public/queries'; +import { SecurityService, SpacesService } from '../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; +import { PINGS_DATE_RANGE_END, PINGS_DATE_RANGE_START } from './constants'; + +// eslint-disable-next-line import/no-default-export +export default function featureControlsTests({ getService }: KibanaFunctionalTestDefaultProviders) { + const supertest = getService('supertestWithoutAuth'); + const security: SecurityService = getService('security'); + const spaces: SpacesService = getService('spaces'); + + const expect404 = (result: any) => { + expect(result.error).to.be(undefined); + expect(result.response).not.to.be(undefined); + expect(result.response).to.have.property('statusCode', 404); + }; + + const expectResponse = (result: any) => { + expect(result.error).to.be(undefined); + expect(result.response).not.to.be(undefined); + expect(result.response).to.have.property('statusCode', 200); + }; + + const executeGraphQLQuery = async (username: string, password: string, spaceId?: string) => { + const basePath = spaceId ? `/s/${spaceId}` : ''; + const getDocCountQuery = { + operationName: null, + query: docCountQueryString, + variables: {}, + }; + + return await supertest + .post(`${basePath}/api/uptime/graphql`) + .auth(username, password) + .set('kbn-xsrf', 'foo') + .send({ ...getDocCountQuery }) + .then((response: any) => ({ error: undefined, response })) + .catch((error: any) => ({ error, response: undefined })); + }; + + const executeIsValidRequest = async (username: string, password: string, spaceId?: string) => { + const basePath = spaceId ? `/s/${spaceId}` : ''; + + return await supertest + .get(`${basePath}/api/uptime/is_valid`) + .auth(username, password) + .set('kbn-xsrf', 'foo') + .then((response: any) => ({ error: undefined, response })) + .catch((error: any) => ({ error, response: undefined })); + }; + + const executePingsRequest = async (username: string, password: string, spaceId?: string) => { + const basePath = spaceId ? `/s/${spaceId}` : ''; + + return await supertest + .get( + `${basePath}/api/uptime/pings?sort=desc&dateRangeStart=${PINGS_DATE_RANGE_START}&dateRangeEnd=${PINGS_DATE_RANGE_END}` + ) + .auth(username, password) + .set('kbn-xsrf', 'foo') + .then((response: any) => ({ error: undefined, response })) + .catch((error: any) => ({ error, response: undefined })); + }; + + describe('feature controls', () => { + it(`APIs can't be accessed by heartbeat-* read privileges role`, async () => { + const username = 'logstash_read'; + const roleName = 'logstash_read'; + const password = `${username}-password`; + try { + await security.role.create(roleName, { + elasticsearch: { + indices: [ + { + names: ['heartbeat-*'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + }); + + await security.user.create(username, { + password, + roles: [roleName], + full_name: 'a kibana user', + }); + + const graphQLResult = await executeGraphQLQuery(username, password); + expect404(graphQLResult); + + const isValidResult = await executeIsValidRequest(username, password); + expect404(isValidResult); + + const pingsResult = await executePingsRequest(username, password); + expect404(pingsResult); + } finally { + await security.role.delete(roleName); + await security.user.delete(username); + } + }); + + it('APIs can be accessed global all with heartbeat-* read privileges role', async () => { + const username = 'global_all'; + const roleName = 'global_all'; + const password = `${username}-password`; + try { + await security.role.create(roleName, { + elasticsearch: { + indices: [ + { + names: ['heartbeat-*'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + kibana: [ + { + base: ['all'], + spaces: ['*'], + }, + ], + }); + + await security.user.create(username, { + password, + roles: [roleName], + full_name: 'a kibana user', + }); + + const graphQLResult = await executeGraphQLQuery(username, password); + expectResponse(graphQLResult); + + const isValidResult = await executeIsValidRequest(username, password); + expectResponse(isValidResult); + + const pingsResult = await executePingsRequest(username, password); + expectResponse(pingsResult); + } finally { + await security.role.delete(roleName); + await security.user.delete(username); + } + }); + + // this could be any role which doesn't have access to the uptime feature + it(`APIs can't be accessed by dashboard all with heartbeat-* read privileges role`, async () => { + const username = 'dashboard_all'; + const roleName = 'dashboard_all'; + const password = `${username}-password`; + try { + await security.role.create(roleName, { + elasticsearch: { + indices: [ + { + names: ['heartbeat-*'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + kibana: [ + { + feature: { + dashboard: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create(username, { + password, + roles: [roleName], + full_name: 'a kibana user', + }); + + const graphQLResult = await executeGraphQLQuery(username, password); + expect404(graphQLResult); + + const isValidResult = await executeIsValidRequest(username, password); + expect404(isValidResult); + + const pingsResult = await executePingsRequest(username, password); + expect404(pingsResult); + } finally { + await security.role.delete(roleName); + await security.user.delete(username); + } + }); + + describe('spaces', () => { + // the following tests create a user_1 which has uptime read access to space_1 and dashboard all access to space_2 + const space1Id = 'space_1'; + const space2Id = 'space_2'; + + const roleName = 'user_1'; + const username = 'user_1'; + const password = 'user_1-password'; + + before(async () => { + await spaces.create({ + id: space1Id, + name: space1Id, + disabledFeatures: [], + }); + await spaces.create({ + id: space2Id, + name: space2Id, + disabledFeatures: [], + }); + await security.role.create(roleName, { + elasticsearch: { + indices: [ + { + names: ['heartbeat-*'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + kibana: [ + { + feature: { + uptime: ['read'], + }, + spaces: [space1Id], + }, + { + feature: { + dashboard: ['all'], + }, + spaces: [space2Id], + }, + ], + }); + await security.user.create(username, { + password, + roles: [roleName], + }); + }); + + after(async () => { + await spaces.delete(space1Id); + await spaces.delete(space2Id); + await security.role.delete(roleName); + await security.user.delete(username); + }); + + it('user_1 can access APIs in space_1', async () => { + const graphQLResult = await executeGraphQLQuery(username, password, space1Id); + expectResponse(graphQLResult); + + const isValidResult = await executeIsValidRequest(username, password, space1Id); + expectResponse(isValidResult); + + const pingsResult = await executePingsRequest(username, password, space1Id); + expectResponse(pingsResult); + }); + + it(`user_1 can't access APIs in space_2`, async () => { + const graphQLResult = await executeGraphQLQuery(username, password); + expect404(graphQLResult); + + const isValidResult = await executeIsValidRequest(username, password); + expect404(isValidResult); + + const pingsResult = await executePingsRequest(username, password); + expect404(pingsResult); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/uptime/get_all_pings.js b/x-pack/test/api_integration/apis/uptime/get_all_pings.js index ccb4f4584ca1..4213684c919a 100644 --- a/x-pack/test/api_integration/apis/uptime/get_all_pings.js +++ b/x-pack/test/api_integration/apis/uptime/get_all_pings.js @@ -6,12 +6,11 @@ import moment from 'moment'; import expect from '@kbn/expect'; +import { PINGS_DATE_RANGE_START, PINGS_DATE_RANGE_END } from './constants'; export default function ({ getService }) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - let dateRangeStart = moment('2018-10-30T00:00:23.889Z').valueOf(); - let dateRangeEnd = moment('2018-10-31T00:00:00.889Z').valueOf(); describe('get_all_pings', () => { const archive = 'uptime/pings'; @@ -21,7 +20,7 @@ export default function ({ getService }) { it('should get all pings stored in index', async () => { const { body: apiResponse } = await supertest - .get(`/api/uptime/pings?sort=desc&dateRangeStart=${dateRangeStart}&dateRangeEnd=${dateRangeEnd}`) + .get(`/api/uptime/pings?sort=desc&dateRangeStart=${PINGS_DATE_RANGE_START}&dateRangeEnd=${PINGS_DATE_RANGE_END}`) .expect(200); expect(apiResponse.total).to.be(2); @@ -32,7 +31,7 @@ export default function ({ getService }) { it('should sort pings according to timestamp', async () => { const { body: apiResponse } = await supertest .get( - `/api/uptime/pings?sort=asc&dateRangeStart=${dateRangeStart}&dateRangeEnd=${dateRangeEnd}` + `/api/uptime/pings?sort=asc&dateRangeStart=${PINGS_DATE_RANGE_START}&dateRangeEnd=${PINGS_DATE_RANGE_END}` ) .expect(200); @@ -45,7 +44,7 @@ export default function ({ getService }) { it('should return results of n length', async () => { const { body: apiResponse } = await supertest .get( - `/api/uptime/pings?sort=desc&size=1&dateRangeStart=${dateRangeStart}&dateRangeEnd=${dateRangeEnd}` + `/api/uptime/pings?sort=desc&size=1&dateRangeStart=${PINGS_DATE_RANGE_START}&dateRangeEnd=${PINGS_DATE_RANGE_END}` ) .expect(200); @@ -55,8 +54,8 @@ export default function ({ getService }) { }); it('should miss pings outside of date range', async () => { - dateRangeStart = moment('2002-01-01').valueOf(); - dateRangeEnd = moment('2002-01-02').valueOf(); + const dateRangeStart = moment('2002-01-01').valueOf(); + const dateRangeEnd = moment('2002-01-02').valueOf(); const { body: apiResponse } = await supertest .get(`/api/uptime/pings?dateRangeStart=${dateRangeStart}&dateRangeEnd=${dateRangeEnd}`) .expect(200); diff --git a/x-pack/test/api_integration/apis/uptime/index.js b/x-pack/test/api_integration/apis/uptime/index.js index eba67c5f9d50..8f4e4ab9a7ea 100644 --- a/x-pack/test/api_integration/apis/uptime/index.js +++ b/x-pack/test/api_integration/apis/uptime/index.js @@ -14,6 +14,7 @@ export default function ({ getService, loadTestFile }) { ignore: [404], })); + loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./get_all_pings')); loadTestFile(require.resolve('./graphql')); }); diff --git a/x-pack/test/api_integration/apis/xpack_main/features/features.ts b/x-pack/test/api_integration/apis/xpack_main/features/features.ts new file mode 100644 index 000000000000..265ce5f47583 --- /dev/null +++ b/x-pack/test/api_integration/apis/xpack_main/features/features.ts @@ -0,0 +1,49 @@ +/* + * 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 { Feature } from '../../../../../plugins/xpack_main/types'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getService }: KibanaFunctionalTestDefaultProviders) { + const supertest = getService('supertest'); + + describe('/api/features', () => { + describe('with trial license', () => { + it('should return a full feature set', async () => { + const { body } = await supertest + .get('/api/features/v1') + .set('kbn-xsrf', 'xxx') + .expect(200); + + expect(body).to.be.an(Array); + + const featureIds = body.map((b: Feature) => b.id); + expect(featureIds.sort()).to.eql( + [ + 'discover', + 'visualize', + 'dashboard', + 'dev_tools', + 'advancedSettings', + 'indexPatterns', + 'timelion', + 'graph', + 'monitoring', + 'ml', + 'apm', + 'canvas', + 'infrastructure', + 'logs', + 'maps', + 'uptime', + ].sort() + ); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/xpack_main/features/index.ts b/x-pack/test/api_integration/apis/xpack_main/features/index.ts new file mode 100644 index 000000000000..fdf7abc9cb62 --- /dev/null +++ b/x-pack/test/api_integration/apis/xpack_main/features/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { + describe('Features', () => { + loadTestFile(require.resolve('./features')); + }); +} diff --git a/x-pack/test/api_integration/apis/xpack_main/index.js b/x-pack/test/api_integration/apis/xpack_main/index.js index b8404d7015e7..3c73bdfc6ec6 100644 --- a/x-pack/test/api_integration/apis/xpack_main/index.js +++ b/x-pack/test/api_integration/apis/xpack_main/index.js @@ -6,6 +6,7 @@ export default function ({ loadTestFile }) { describe('xpack_main', () => { + loadTestFile(require.resolve('./features')); loadTestFile(require.resolve('./telemetry')); loadTestFile(require.resolve('./settings')); }); diff --git a/x-pack/test/api_integration/config.js b/x-pack/test/api_integration/config.js index 934d50c7cce6..a9da26722c82 100644 --- a/x-pack/test/api_integration/config.js +++ b/x-pack/test/api_integration/config.js @@ -9,9 +9,15 @@ import { EsSupertestWithoutAuthProvider, SupertestWithoutAuthProvider, UsageAPIProvider, - InfraOpsGraphQLProvider + InfraOpsGraphQLClientProvider, + InfraOpsGraphQLClientFactoryProvider, } from './services'; +import { + SecurityServiceProvider, + SpacesServiceProvider, +} from '../common/services'; + export default async function ({ readConfigFile }) { const kibanaAPITestsConfig = await readConfigFile(require.resolve('../../../test/api_integration/config.js')); @@ -26,12 +32,15 @@ export default async function ({ readConfigFile }) { esSupertest: kibanaAPITestsConfig.get('services.esSupertest'), supertestWithoutAuth: SupertestWithoutAuthProvider, esSupertestWithoutAuth: EsSupertestWithoutAuthProvider, - infraOpsGraphQLClient: InfraOpsGraphQLProvider, + infraOpsGraphQLClient: InfraOpsGraphQLClientProvider, + infraOpsGraphQLClientFactory: InfraOpsGraphQLClientFactoryProvider, es: EsProvider, esArchiver: kibanaCommonConfig.get('services.esArchiver'), usageAPI: UsageAPIProvider, kibanaServer: kibanaCommonConfig.get('services.kibanaServer'), chance: kibanaAPITestsConfig.get('services.chance'), + security: SecurityServiceProvider, + spaces: SpacesServiceProvider, }, esArchiver: xPackFunctionalTestsConfig.get('esArchiver'), junit: { diff --git a/x-pack/test/api_integration/services/index.js b/x-pack/test/api_integration/services/index.js index 96d349f84862..87325c3b4ad9 100644 --- a/x-pack/test/api_integration/services/index.js +++ b/x-pack/test/api_integration/services/index.js @@ -8,4 +8,4 @@ export { EsProvider } from './es'; export { EsSupertestWithoutAuthProvider } from './es_supertest_without_auth'; export { SupertestWithoutAuthProvider } from './supertest_without_auth'; export { UsageAPIProvider } from './usage_api'; -export { InfraOpsGraphQLProvider } from './infraops_graphql_client'; +export { InfraOpsGraphQLClientProvider, InfraOpsGraphQLClientFactoryProvider } from './infraops_graphql_client'; diff --git a/x-pack/test/api_integration/services/infraops_graphql_client.js b/x-pack/test/api_integration/services/infraops_graphql_client.js index 81cdc442f4fb..52f064ee9e19 100644 --- a/x-pack/test/api_integration/services/infraops_graphql_client.js +++ b/x-pack/test/api_integration/services/infraops_graphql_client.js @@ -12,23 +12,34 @@ import { HttpLink } from 'apollo-link-http'; import introspectionQueryResultData from '../../../plugins/infra/public/graphql/introspection.json'; -export function InfraOpsGraphQLProvider({ getService }) { +export function InfraOpsGraphQLClientProvider({ getService }) { + return new InfraOpsGraphQLClientFactoryProvider({ getService })(); +} + +export function InfraOpsGraphQLClientFactoryProvider({ getService }) { const config = getService('config'); - const kbnURL = formatUrl(config.get('servers.kibana')); + const [superUsername, superPassword] = config.get('servers.elasticsearch.auth').split(':'); - return new ApolloClient({ - cache: new InMemoryCache({ - fragmentMatcher: new IntrospectionFragmentMatcher({ - introspectionQueryResultData, - }), - }), - link: new HttpLink({ + return function ({ username = superUsername, password = superPassword, basePath = null } = {}) { + const kbnURLWithoutAuth = formatUrl({ ...config.get('servers.kibana'), auth: false }); + + const httpLink = new HttpLink({ credentials: 'same-origin', fetch, headers: { 'kbn-xsrf': 'xxx', + authorization: `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`, }, - uri: `${kbnURL}/api/infra/graphql`, - }), - }); + uri: `${kbnURLWithoutAuth}${basePath || ''}/api/infra/graphql`, + }); + + return new ApolloClient({ + cache: new InMemoryCache({ + fragmentMatcher: new IntrospectionFragmentMatcher({ + introspectionQueryResultData, + }), + }), + link: httpLink, + }); + }; } diff --git a/x-pack/test/common/services/index.ts b/x-pack/test/common/services/index.ts new file mode 100644 index 000000000000..37b588064856 --- /dev/null +++ b/x-pack/test/common/services/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { SecurityServiceProvider, SecurityService } from './security'; +export { SpacesServiceProvider, SpacesService } from './spaces'; diff --git a/x-pack/test/common/services/security/index.ts b/x-pack/test/common/services/security/index.ts new file mode 100644 index 000000000000..e5845098f231 --- /dev/null +++ b/x-pack/test/common/services/security/index.ts @@ -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 { format as formatUrl } from 'url'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; +import { LogService } from '../../../types/services'; +import { Role } from './role'; +import { User } from './user'; + +export class SecurityService { + public role: Role; + public user: User; + + constructor(url: string, log: LogService) { + this.role = new Role(url, log); + this.user = new User(url, log); + } +} + +export function SecurityServiceProvider({ getService }: KibanaFunctionalTestDefaultProviders) { + const log = getService('log'); + const config = getService('config'); + const url = formatUrl(config.get('servers.kibana')); + + return new SecurityService(url, log); +} diff --git a/x-pack/test/common/services/security/role.ts b/x-pack/test/common/services/security/role.ts new file mode 100644 index 000000000000..e96f85c70233 --- /dev/null +++ b/x-pack/test/common/services/security/role.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import axios, { AxiosInstance } from 'axios'; +import util from 'util'; +import { LogService } from '../../../types/services'; + +export class Role { + private log: LogService; + private axios: AxiosInstance; + + constructor(url: string, log: LogService) { + this.log = log; + this.axios = axios.create({ + headers: { 'kbn-xsrf': 'x-pack/ftr/services/security/role' }, + baseURL: url, + maxRedirects: 0, + validateStatus: () => true, // we do our own validation below and throw better error messages + }); + } + + public async create(name: string, role: any) { + this.log.debug(`creating role ${name}`); + const { data, status, statusText } = await this.axios.put(`/api/security/role/${name}`, role); + if (status !== 204) { + throw new Error( + `Expected status code of 204, received ${status} ${statusText}: ${util.inspect(data)}` + ); + } + this.log.debug(`created role ${name}`); + } + + public async delete(name: string) { + this.log.debug(`deleting role ${name}`); + const { data, status, statusText } = await this.axios.delete(`/api/security/role/${name}`); + if (status !== 204 && status !== 404) { + throw new Error( + `Expected status code of 204 or 404, received ${status} ${statusText}: ${util.inspect( + data + )}` + ); + } + this.log.debug(`deleted role ${name}`); + } +} diff --git a/x-pack/test/common/services/security/user.ts b/x-pack/test/common/services/security/user.ts new file mode 100644 index 000000000000..2685222150df --- /dev/null +++ b/x-pack/test/common/services/security/user.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import axios, { AxiosInstance } from 'axios'; +import util from 'util'; +import { LogService } from '../../../types/services'; + +export class User { + private log: LogService; + private axios: AxiosInstance; + + constructor(url: string, log: LogService) { + this.log = log; + this.axios = axios.create({ + headers: { 'kbn-xsrf': 'x-pack/ftr/services/security/user' }, + baseURL: url, + maxRedirects: 0, + validateStatus: () => true, // we do our own validation below and throw better error messages + }); + } + + public async create(username: string, user: any) { + this.log.debug(`creating user ${username}`); + const { data, status, statusText } = await this.axios.post( + `/api/security/v1/users/${username}`, + { + username, + ...user, + } + ); + if (status !== 200) { + throw new Error( + `Expected status code of 200, received ${status} ${statusText}: ${util.inspect(data)}` + ); + } + this.log.debug(`created user ${username}`); + } + + public async delete(username: string) { + this.log.debug(`deleting user ${username}`); + const { data, status, statusText } = await this.axios.delete( + `/api/security/v1/users/${username}` + ); + if (status !== 204) { + throw new Error( + `Expected status code of 204, received ${status} ${statusText}: ${util.inspect(data)}` + ); + } + this.log.debug(`deleted user ${username}`); + } +} diff --git a/x-pack/test/common/services/spaces/index.ts b/x-pack/test/common/services/spaces/index.ts new file mode 100644 index 000000000000..08d7a676e791 --- /dev/null +++ b/x-pack/test/common/services/spaces/index.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import axios, { AxiosInstance } from 'axios'; +import { format as formatUrl } from 'url'; +import util from 'util'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; +import { LogService } from '../../../types/services'; + +export class SpacesService { + private log: LogService; + private axios: AxiosInstance; + + constructor(url: string, log: LogService) { + this.log = log; + this.axios = axios.create({ + headers: { 'kbn-xsrf': 'x-pack/ftr/services/spaces/space' }, + baseURL: url, + maxRedirects: 0, + validateStatus: () => true, // we do our own validation below and throw better error messages + }); + } + + public async create(space: any) { + this.log.debug('creating space'); + const { data, status, statusText } = await this.axios.post('/api/spaces/space', space); + + if (status !== 200) { + throw new Error( + `Expected status code of 200, received ${status} ${statusText}: ${util.inspect(data)}` + ); + } + this.log.debug('created space'); + } + + public async delete(spaceId: string) { + this.log.debug(`deleting space: ${spaceId}`); + const { data, status, statusText } = await this.axios.delete(`/api/spaces/space/${spaceId}`); + + if (status !== 204) { + throw new Error( + `Expected status code of 204, received ${status} ${statusText}: ${util.inspect(data)}` + ); + } + this.log.debug(`deleted space: ${spaceId}`); + } +} + +export function SpacesServiceProvider({ getService }: KibanaFunctionalTestDefaultProviders) { + const log = getService('log'); + const config = getService('config'); + const url = formatUrl(config.get('servers.kibana')); + return new SpacesService(url, log); +} diff --git a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts new file mode 100644 index 000000000000..1d345eea7037 --- /dev/null +++ b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts @@ -0,0 +1,190 @@ +/* + * 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 { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'settings', 'security', 'spaceSelector']); + const appsMenu = getService('appsMenu'); + const testSubjects = getService('testSubjects'); + + describe('security feature controls', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + }); + + describe('global advanced_settings all privileges', () => { + before(async () => { + await security.role.create('global_advanced_settings_all_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + advancedSettings: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_advanced_settings_all_user', { + password: 'global_advanced_settings_all_user-password', + roles: ['global_advanced_settings_all_role'], + full_name: 'test user', + }); + + await PageObjects.security.logout(); + + await PageObjects.security.login( + 'global_advanced_settings_all_user', + 'global_advanced_settings_all_user-password', + { + expectSpaceSelector: false, + } + ); + + await kibanaServer.uiSettings.replace({}); + await PageObjects.settings.navigateTo(); + }); + + after(async () => { + await Promise.all([ + security.role.delete('global_advanced_settings_all_role'), + security.user.delete('global_advanced_settings_all_user'), + PageObjects.security.logout(), + ]); + }); + + it('shows management navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Management']); + }); + + it(`allows settings to be changed`, async () => { + await PageObjects.settings.clickKibanaSettings(); + await PageObjects.settings.setAdvancedSettingsSelect('dateFormat:tz', 'America/Phoenix'); + const advancedSetting = await PageObjects.settings.getAdvancedSettings('dateFormat:tz'); + expect(advancedSetting).to.be('America/Phoenix'); + }); + }); + + describe('global advanced_settings read-only privileges', () => { + before(async () => { + await security.role.create('global_advanced_settings_read_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + advancedSettings: ['read'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_advanced_settings_read_user', { + password: 'global_advanced_settings_read_user-password', + roles: ['global_advanced_settings_read_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'global_advanced_settings_read_user', + 'global_advanced_settings_read_user-password', + { + expectSpaceSelector: false, + } + ); + + await kibanaServer.uiSettings.replace({}); + await PageObjects.settings.navigateTo(); + }); + + after(async () => { + await security.role.delete('global_advanced_settings_read_role'); + await security.user.delete('global_advanced_settings_read_user'); + }); + + it('shows Management navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Management']); + }); + + it(`does not allow settings to be changed`, async () => { + await PageObjects.settings.clickKibanaSettings(); + await PageObjects.settings.expectDisabledAdvancedSetting('dateFormat:tz'); + }); + }); + + describe('no advanced_settings privileges', () => { + before(async () => { + await security.role.create('no_advanced_settings_privileges_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + discover: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('no_advanced_settings_privileges_user', { + password: 'no_advanced_settings_privileges_user-password', + roles: ['no_advanced_settings_privileges_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'no_advanced_settings_privileges_user', + 'no_advanced_settings_privileges_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('no_advanced_settings_privileges_role'); + await security.user.delete('no_advanced_settings_privileges_user'); + }); + + it('shows Management navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Discover', 'Management']); + }); + + it(`does not allow navigation to advanced settings; redirects to Kibana home`, async () => { + await PageObjects.common.navigateToActualUrl('kibana', 'management/kibana/settings', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('homeApp', 10000); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts new file mode 100644 index 000000000000..6b32bfd18747 --- /dev/null +++ b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { SpacesService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const spacesService: SpacesService = getService('spaces'); + const PageObjects = getPageObjects(['common', 'settings', 'security', 'spaceSelector']); + const testSubjects = getService('testSubjects'); + const appsMenu = getService('appsMenu'); + + describe('spaces feature controls', () => { + before(async () => { + await esArchiver.loadIfNeeded('logstash_functional'); + }); + + describe('space with no features disabled', () => { + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('empty_kibana'); + + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: [], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('empty_kibana'); + }); + + it('shows Management navlink', async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.contain('Management'); + }); + + it(`allows settings to be changed`, async () => { + await PageObjects.common.navigateToActualUrl('kibana', 'management/kibana/settings', { + basePath: `/s/custom_space`, + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await PageObjects.settings.setAdvancedSettingsSelect('dateFormat:tz', 'America/Phoenix'); + const advancedSetting = await PageObjects.settings.getAdvancedSettings('dateFormat:tz'); + expect(advancedSetting).to.be('America/Phoenix'); + }); + }); + + describe('space with Advanced Settings disabled', () => { + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('empty_kibana'); + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: ['advancedSettings'], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('empty_kibana'); + }); + + it(`redirects to Kibana home`, async () => { + await PageObjects.common.navigateToActualUrl('kibana', 'management/kibana/settings', { + basePath: `/s/custom_space`, + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('homeApp', 10000); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/advanced_settings/index.ts b/x-pack/test/functional/apps/advanced_settings/index.ts new file mode 100644 index 000000000000..43bd8e015791 --- /dev/null +++ b/x-pack/test/functional/apps/advanced_settings/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function advancedSettingsApp({ + loadTestFile, +}: KibanaFunctionalTestDefaultProviders) { + describe('Advanced Settings', function canvasAppTestSuite() { + this.tags('ciGroup2'); // CI requires tags ヽ(゜Q。)ノ? + loadTestFile(require.resolve('./feature_controls/advanced_settings_security')); + loadTestFile(require.resolve('./feature_controls/advanced_settings_spaces')); + }); +} diff --git a/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts b/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts new file mode 100644 index 000000000000..ee97fd1806d2 --- /dev/null +++ b/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts @@ -0,0 +1,173 @@ +/* + * 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 { SecurityService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const security: SecurityService = getService('security'); + const PageObjects = getPageObjects(['common', 'error', 'security']); + const testSubjects = getService('testSubjects'); + const appsMenu = getService('appsMenu'); + + describe('security', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + // ensure we're logged out so we can login as the appropriate users + await PageObjects.security.forceLogout(); + }); + + after(async () => { + // logout, so the other tests don't accidentally run as the custom users we're testing below + await PageObjects.security.forceLogout(); + }); + + describe('global apm all privileges', () => { + before(async () => { + await security.role.create('global_apm_all_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + apm: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_apm_all_user', { + password: 'global_apm_all_user-password', + roles: ['global_apm_all_role'], + full_name: 'test user', + }); + + await PageObjects.security.login('global_apm_all_user', 'global_apm_all_user-password', { + expectSpaceSelector: false, + }); + }); + + after(async () => { + await security.role.delete('global_apm_all_role'); + await security.user.delete('global_apm_all_user'); + }); + + it('shows apm navlink', async () => { + const navLinks = await appsMenu.readLinks(); + expect(navLinks.map((link: Record) => link.text)).to.eql([ + 'APM', + 'Management', + ]); + }); + + it('can navigate to APM app', async () => { + await PageObjects.common.navigateToApp('apm'); + await testSubjects.existOrFail('apmMainContainer', 10000); + }); + }); + + describe('global apm read-only privileges', () => { + before(async () => { + await security.role.create('global_apm_read_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + apm: ['read'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_apm_read_user', { + password: 'global_apm_read_user-password', + roles: ['global_apm_read_role'], + full_name: 'test user', + }); + + await PageObjects.security.login('global_apm_read_user', 'global_apm_read_user-password', { + expectSpaceSelector: false, + }); + }); + + after(async () => { + await security.role.delete('global_apm_read_role'); + await security.user.delete('global_apm_read_user'); + }); + + it('shows apm navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['APM', 'Management']); + }); + + it('can navigate to APM app', async () => { + await PageObjects.common.navigateToApp('apm'); + await testSubjects.existOrFail('apmMainContainer', 10000); + }); + }); + + describe('no apm privileges', () => { + before(async () => { + await security.role.create('no_apm_privileges_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + dashboard: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('no_apm_privileges_user', { + password: 'no_apm_privileges_user-password', + roles: ['no_apm_privileges_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'no_apm_privileges_user', + 'no_apm_privileges_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('no_apm_privileges_role'); + await security.user.delete('no_apm_privileges_user'); + }); + + it(`doesn't show APM navlink`, async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).not.to.contain('APM'); + }); + + it(`renders not found page`, async () => { + await PageObjects.common.navigateToUrl('apm', '', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await PageObjects.error.expectNotFound(); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/apm/feature_controls/apm_spaces.ts b/x-pack/test/functional/apps/apm/feature_controls/apm_spaces.ts new file mode 100644 index 000000000000..ff32bcc5c6c8 --- /dev/null +++ b/x-pack/test/functional/apps/apm/feature_controls/apm_spaces.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { SpacesService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const spacesService: SpacesService = getService('spaces'); + const PageObjects = getPageObjects(['common', 'error', 'timePicker', 'security']); + const testSubjects = getService('testSubjects'); + const appsMenu = getService('appsMenu'); + + describe('spaces', () => { + describe('space with no features disabled', () => { + before(async () => { + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: [], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + }); + + it('shows apm navlink', async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.contain('APM'); + }); + + it('can navigate to Uptime app', async () => { + await PageObjects.common.navigateToApp('apm'); + await testSubjects.existOrFail('apmMainContainer', 10000); + }); + }); + + describe('space with Uptime disabled', () => { + before(async () => { + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: ['apm'], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + }); + + it(`doesn't show apm navlink`, async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).not.to.contain('APM'); + }); + + it(`renders not found page`, async () => { + await PageObjects.common.navigateToUrl('apm', '', { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await PageObjects.error.expectNotFound(); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/apm/feature_controls/index.ts b/x-pack/test/functional/apps/apm/feature_controls/index.ts new file mode 100644 index 000000000000..29f87d9c6a07 --- /dev/null +++ b/x-pack/test/functional/apps/apm/feature_controls/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { + describe('feature controls', () => { + loadTestFile(require.resolve('./apm_security')); + loadTestFile(require.resolve('./apm_spaces')); + }); +} diff --git a/x-pack/test/functional/apps/apm/index.ts b/x-pack/test/functional/apps/apm/index.ts new file mode 100644 index 000000000000..d8f417774111 --- /dev/null +++ b/x-pack/test/functional/apps/apm/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { + describe('APM', function() { + this.tags('ciGroup6'); + loadTestFile(require.resolve('./feature_controls')); + }); +} diff --git a/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts b/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts new file mode 100644 index 000000000000..7a934b5dcd8b --- /dev/null +++ b/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts @@ -0,0 +1,250 @@ +/* + * 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 { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'canvas', 'security', 'spaceSelector']); + const find = getService('find'); + const appsMenu = getService('appsMenu'); + + describe('security feature controls', () => { + before(async () => { + await esArchiver.load('canvas/default'); + }); + + after(async () => { + await esArchiver.unload('canvas/default'); + }); + + describe('global canvas all privileges', () => { + before(async () => { + await security.role.create('global_canvas_all_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + canvas: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_canvas_all_user', { + password: 'global_canvas_all_user-password', + roles: ['global_canvas_all_role'], + full_name: 'test user', + }); + + await PageObjects.security.logout(); + + await PageObjects.security.login( + 'global_canvas_all_user', + 'global_canvas_all_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await Promise.all([ + security.role.delete('global_canvas_all_role'), + security.user.delete('global_canvas_all_user'), + PageObjects.security.logout(), + ]); + }); + + it('shows canvas navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Canvas', 'Management']); + }); + + it(`landing page shows "Create new workpad" button`, async () => { + await PageObjects.common.navigateToActualUrl('canvas', '', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await PageObjects.canvas.expectCreateWorkpadButtonEnabled(); + }); + + it(`allows a workpad to be created`, async () => { + await PageObjects.common.navigateToActualUrl('canvas', 'workpad/create', { + ensureCurrentUrl: true, + showLoginIfPrompted: false, + }); + + await PageObjects.canvas.expectAddElementButton(); + }); + + it(`allows a workpad to be edited`, async () => { + await PageObjects.common.navigateToActualUrl( + 'canvas', + 'workpad/workpad-1705f884-6224-47de-ba49-ca224fe6ec31', + { + ensureCurrentUrl: true, + showLoginIfPrompted: false, + } + ); + + await PageObjects.canvas.expectAddElementButton(); + }); + }); + + describe('global canvas read-only privileges', () => { + before(async () => { + await security.role.create('global_canvas_read_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + canvas: ['read'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_canvas_read_user', { + password: 'global_canvas_read_user-password', + roles: ['global_canvas_read_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'global_canvas_read_user', + 'global_canvas_read_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('global_canvas_read_role'); + await security.user.delete('global_canvas_read_user'); + }); + + it('shows canvas navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Canvas', 'Management']); + }); + + it(`landing page shows disabled "Create new workpad" button`, async () => { + await PageObjects.common.navigateToActualUrl('canvas', '', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await PageObjects.canvas.expectCreateWorkpadButtonDisabled(); + }); + + it(`does not allow a workpad to be created`, async () => { + await PageObjects.common.navigateToActualUrl('canvas', 'workpad/create', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + + // expect redirection to canvas landing + await PageObjects.canvas.expectCreateWorkpadButtonDisabled(); + }); + + it(`does not allow a workpad to be edited`, async () => { + await PageObjects.common.navigateToActualUrl( + 'canvas', + 'workpad/workpad-1705f884-6224-47de-ba49-ca224fe6ec31', + { + ensureCurrentUrl: true, + shouldLoginIfPrompted: false, + } + ); + + await PageObjects.canvas.expectNoAddElementButton(); + }); + }); + + describe('no canvas privileges', () => { + before(async () => { + await security.role.create('no_canvas_privileges_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + discover: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('no_canvas_privileges_user', { + password: 'no_canvas_privileges_user-password', + roles: ['no_canvas_privileges_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'no_canvas_privileges_user', + 'no_canvas_privileges_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('no_canvas_privileges_role'); + await security.user.delete('no_canvas_privileges_user'); + }); + + const getMessageText = async () => + await (await find.byCssSelector('body>pre')).getVisibleText(); + + it(`returns a 404`, async () => { + await PageObjects.common.navigateToActualUrl('canvas', '', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + const messageText = await getMessageText(); + expect(messageText).to.eql( + JSON.stringify({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }) + ); + }); + + it(`create new workpad returns a 404`, async () => { + await PageObjects.common.navigateToActualUrl('canvas', 'workpad/create', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + const messageText = await getMessageText(); + expect(messageText).to.eql( + JSON.stringify({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }) + ); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts b/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts new file mode 100644 index 000000000000..eba2a5c507c9 --- /dev/null +++ b/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts @@ -0,0 +1,150 @@ +/* + * 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 { SpacesService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const spacesService: SpacesService = getService('spaces'); + const PageObjects = getPageObjects(['common', 'canvas', 'security', 'spaceSelector']); + const find = getService('find'); + const appsMenu = getService('appsMenu'); + + describe('spaces feature controls', () => { + before(async () => { + await esArchiver.loadIfNeeded('logstash_functional'); + }); + + describe('space with no features disabled', () => { + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('canvas/default'); + + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: [], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('canvas/default'); + }); + + it('shows canvas navlink', async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.contain('Canvas'); + }); + + it(`landing page shows "Create new workpad" button`, async () => { + await PageObjects.common.navigateToActualUrl('canvas', '', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await PageObjects.canvas.expectCreateWorkpadButtonEnabled(); + }); + + it(`allows a workpad to be created`, async () => { + await PageObjects.common.navigateToActualUrl('canvas', 'workpad/create', { + ensureCurrentUrl: true, + shouldLoginIfPrompted: false, + }); + + await PageObjects.canvas.expectAddElementButton(); + }); + + it(`allows a workpad to be edited`, async () => { + await PageObjects.common.navigateToActualUrl( + 'canvas', + 'workpad/workpad-1705f884-6224-47de-ba49-ca224fe6ec31', + { + ensureCurrentUrl: true, + shouldLoginIfPrompted: false, + } + ); + + await PageObjects.canvas.expectAddElementButton(); + }); + }); + + describe('space with Canvas disabled', () => { + const getMessageText = async () => + await (await find.byCssSelector('body>pre')).getVisibleText(); + + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('spaces/disabled_features'); + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: ['canvas'], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('spaces/disabled_features'); + }); + + it(`doesn't show canvas navlink`, async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).not.to.contain('Canvas'); + }); + + it(`create new workpad returns a 404`, async () => { + await PageObjects.common.navigateToActualUrl('canvas', 'workpad/create', { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + + const messageText = await getMessageText(); + expect(messageText).to.eql( + JSON.stringify({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }) + ); + }); + + it(`edit workpad returns a 404`, async () => { + await PageObjects.common.navigateToActualUrl( + 'canvas', + 'workpad/workpad-1705f884-6224-47de-ba49-ca224fe6ec31', + { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + } + ); + const messageText = await getMessageText(); + expect(messageText).to.eql( + JSON.stringify({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }) + ); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/canvas/index.js b/x-pack/test/functional/apps/canvas/index.js index 6620ee6c26f0..bc33161cc4e9 100644 --- a/x-pack/test/functional/apps/canvas/index.js +++ b/x-pack/test/functional/apps/canvas/index.js @@ -8,5 +8,7 @@ export default function canvasApp({ loadTestFile }) { describe('Canvas app', function canvasAppTestSuite() { this.tags('ciGroup2'); // CI requires tags ヽ(゜Q。)ノ? loadTestFile(require.resolve('./smoke_test')); + loadTestFile(require.resolve('./feature_controls/canvas_security')); + loadTestFile(require.resolve('./feature_controls/canvas_spaces')); }); } diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts new file mode 100644 index 000000000000..0f04a41d8851 --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts @@ -0,0 +1,342 @@ +/* + * 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'; +// eslint-disable-next-line max-len +import { + createDashboardEditUrl, + DashboardConstants, +} from '../../../../../../src/legacy/core_plugins/kibana/public/dashboard/dashboard_constants'; +import { SecurityService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const security: SecurityService = getService('security'); + const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'spaceSelector', 'share']); + const appsMenu = getService('appsMenu'); + const panelActions = getService('dashboardPanelActions'); + const testSubjects = getService('testSubjects'); + + describe('security', () => { + before(async () => { + await esArchiver.load('dashboard/feature_controls/security'); + await esArchiver.loadIfNeeded('logstash_functional'); + + // ensure we're logged out so we can login as the appropriate users + await PageObjects.security.forceLogout(); + }); + + after(async () => { + await esArchiver.unload('dashboard/feature_controls/security'); + + // logout, so the other tests don't accidentally run as the custom users we're testing below + await PageObjects.security.forceLogout(); + }); + + describe('global dashboard all privileges', () => { + before(async () => { + await security.role.create('global_dashboard_all_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + dashboard: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_dashboard_all_user', { + password: 'global_dashboard_all_user-password', + roles: ['global_dashboard_all_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'global_dashboard_all_user', + 'global_dashboard_all_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('global_dashboard_all_role'); + await security.user.delete('global_dashboard_all_user'); + }); + + it('shows dashboard navlink', async () => { + const navLinks = await appsMenu.readLinks(); + expect(navLinks.map((link: Record) => link.text)).to.eql([ + 'Dashboard', + 'Management', + ]); + }); + + it(`landing page shows "Create new Dashboard" button`, async () => { + await PageObjects.common.navigateToActualUrl( + 'kibana', + DashboardConstants.LANDING_PAGE_PATH, + { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + } + ); + await testSubjects.existOrFail('dashboardLandingPage', 10000); + await testSubjects.existOrFail('newItemButton'); + }); + + it(`create new dashboard shows addNew button`, async () => { + await PageObjects.common.navigateToActualUrl( + 'kibana', + DashboardConstants.CREATE_NEW_DASHBOARD_URL, + { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + } + ); + await testSubjects.existOrFail('emptyDashboardAddPanelButton', 10000); + }); + + it(`can view existing Dashboard`, async () => { + await PageObjects.common.navigateToActualUrl('kibana', createDashboardEditUrl('i-exist'), { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('dashboardPanelHeading-APie', 10000); + }); + + it(`does not allow a visualization to be edited`, async () => { + await PageObjects.dashboard.gotoDashboardEditMode('A Dashboard'); + await panelActions.openContextMenu(); + await panelActions.expectMissingEditPanelAction(); + }); + + it(`Permalinks shows create short-url button`, async () => { + await PageObjects.share.openShareMenuItem('Permalinks'); + await PageObjects.share.createShortUrlExistOrFail(); + }); + }); + + describe('global dashboard & visualize all privileges', () => { + before(async () => { + await security.role.create('global_dashboard_visualize_all_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + dashboard: ['all'], + visualize: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_dashboard_visualize_all_user', { + password: 'global_dashboard_visualize_all_user-password', + roles: ['global_dashboard_visualize_all_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'global_dashboard_visualize_all_user', + 'global_dashboard_visualize_all_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('global_dashboard_visualize_all_role'); + await security.user.delete('global_dashboard_visualize_all_user'); + }); + + it(`allows a visualization to be edited`, async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.gotoDashboardEditMode('A Dashboard'); + await panelActions.openContextMenu(); + await panelActions.expectExistsEditPanelAction(); + }); + }); + + describe('global dashboard read-only privileges', () => { + before(async () => { + await security.role.create('global_dashboard_read_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + dashboard: ['read'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_dashboard_read_user', { + password: 'global_dashboard_read_user-password', + roles: ['global_dashboard_read_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'global_dashboard_read_user', + 'global_dashboard_read_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('global_dashboard_read_role'); + await security.user.delete('global_dashboard_read_user'); + }); + + it('shows dashboard navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Dashboard', 'Management']); + }); + + it(`landing page doesn't show "Create new Dashboard" button`, async () => { + await PageObjects.common.navigateToActualUrl( + 'kibana', + DashboardConstants.LANDING_PAGE_PATH, + { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + } + ); + await testSubjects.existOrFail('dashboardLandingPage', 10000); + await testSubjects.missingOrFail('newItemButton'); + }); + + it(`create new dashboard redirects to the home page`, async () => { + await PageObjects.common.navigateToActualUrl( + 'kibana', + DashboardConstants.CREATE_NEW_DASHBOARD_URL, + { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + } + ); + await testSubjects.existOrFail('homeApp', 20000); + }); + + it(`can view existing Dashboard`, async () => { + await PageObjects.common.navigateToActualUrl('kibana', createDashboardEditUrl('i-exist'), { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('dashboardPanelHeading-APie', 10000); + }); + + it(`Permalinks doesn't show create short-url button`, async () => { + await PageObjects.share.openShareMenuItem('Permalinks'); + await PageObjects.share.createShortUrlMissingOrFail(); + }); + }); + + describe('no dashboard privileges', () => { + before(async () => { + await security.role.create('no_dashboard_privileges_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + discover: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('no_dashboard_privileges_user', { + password: 'no_dashboard_privileges_user-password', + roles: ['no_dashboard_privileges_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'no_dashboard_privileges_user', + 'no_dashboard_privileges_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('no_dashboard_privileges_role'); + await security.user.delete('no_dashboard_privileges_user'); + }); + + it(`doesn't show dashboard navLink`, async () => { + const navLinks = await appsMenu.readLinks(); + expect(navLinks.map((navLink: any) => navLink.text)).to.not.contain(['Dashboard']); + }); + + it(`landing page redirects to the home page`, async () => { + await PageObjects.common.navigateToActualUrl( + 'kibana', + DashboardConstants.LANDING_PAGE_PATH, + { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + } + ); + await testSubjects.existOrFail('homeApp', 10000); + }); + + it(`create new dashboard redirects to the home page`, async () => { + await PageObjects.common.navigateToActualUrl( + 'kibana', + DashboardConstants.CREATE_NEW_DASHBOARD_URL, + { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + } + ); + await testSubjects.existOrFail('homeApp', 20000); + }); + + it(`edit dashboard for object which doesn't exist redirects to the home page`, async () => { + await PageObjects.common.navigateToActualUrl( + 'kibana', + createDashboardEditUrl('i-dont-exist'), + { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + } + ); + await testSubjects.existOrFail('homeApp', 10000); + }); + + it(`edit dashboard for object which exists redirects to the home page`, async () => { + await PageObjects.common.navigateToActualUrl('kibana', createDashboardEditUrl('i-exist'), { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('homeApp', 10000); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_spaces.ts b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_spaces.ts new file mode 100644 index 000000000000..227dd43da36f --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_spaces.ts @@ -0,0 +1,155 @@ +/* + * 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'; +// eslint-disable-next-line max-len +import { + createDashboardEditUrl, + DashboardConstants, +} from '../../../../../../src/legacy/core_plugins/kibana/public/dashboard/dashboard_constants'; +import { SpacesService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const spacesService: SpacesService = getService('spaces'); + const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'spaceSelector']); + const appsMenu = getService('appsMenu'); + const testSubjects = getService('testSubjects'); + + describe('spaces', () => { + before(async () => { + await esArchiver.loadIfNeeded('logstash_functional'); + }); + + describe('space with no features disabled', () => { + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('dashboard/feature_controls/spaces'); + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: [], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('dashboard/feature_controls/spaces'); + }); + + it('shows dashboard navlink', async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.contain('Dashboard'); + }); + + it(`landing page shows "Create new Dashboard" button`, async () => { + await PageObjects.common.navigateToActualUrl( + 'kibana', + DashboardConstants.LANDING_PAGE_PATH, + { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + } + ); + await testSubjects.existOrFail('dashboardLandingPage', 10000); + await testSubjects.existOrFail('newItemButton'); + }); + + it(`create new dashboard shows addNew button`, async () => { + await PageObjects.common.navigateToActualUrl( + 'kibana', + DashboardConstants.CREATE_NEW_DASHBOARD_URL, + { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + } + ); + await testSubjects.existOrFail('emptyDashboardAddPanelButton', 10000); + }); + + it(`can view existing Dashboard`, async () => { + await PageObjects.common.navigateToActualUrl('kibana', createDashboardEditUrl('i-exist'), { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('dashboardPanelHeading-APie', 10000); + }); + }); + + describe('space with Dashboard disabled', () => { + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('dashboard/feature_controls/spaces'); + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: ['dashboard'], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('dashboard/feature_controls/spaces'); + }); + + it(`doesn't show dashboard navlink`, async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).not.to.contain('Dashboard'); + }); + + it(`create new dashboard redirects to the home page`, async () => { + await PageObjects.common.navigateToActualUrl( + 'kibana', + DashboardConstants.CREATE_NEW_DASHBOARD_URL, + { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + } + ); + await testSubjects.existOrFail('homeApp', 10000); + }); + + it(`edit dashboard for object which doesn't exist redirects to the home page`, async () => { + await PageObjects.common.navigateToActualUrl( + 'kibana', + createDashboardEditUrl('i-dont-exist'), + { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + } + ); + await testSubjects.existOrFail('homeApp', 10000); + }); + + it(`edit dashboard for object which exists redirects to the home page`, async () => { + await PageObjects.common.navigateToActualUrl('kibana', createDashboardEditUrl('i-exist'), { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('homeApp', 10000); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/index.ts b/x-pack/test/functional/apps/dashboard/feature_controls/index.ts new file mode 100644 index 000000000000..37179d4e1dc7 --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/feature_controls/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { + describe('feature controls', () => { + loadTestFile(require.resolve('./dashboard_security')); + loadTestFile(require.resolve('./dashboard_spaces')); + }); +} diff --git a/x-pack/test/functional/apps/dashboard/index.ts b/x-pack/test/functional/apps/dashboard/index.ts new file mode 100644 index 000000000000..5cea2fda9644 --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { + describe('dashboard', function() { + this.tags('ciGroup3'); + + loadTestFile(require.resolve('./feature_controls')); + }); +} diff --git a/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js b/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js index 6b7a20ad333d..4b3f11dbf2c4 100644 --- a/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js +++ b/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js @@ -25,6 +25,7 @@ export default function ({ getService, getPageObjects }) { 'header', 'settings', 'timePicker', + 'share', ]); const dashboardName = 'Dashboard View Mode Test Dashboard'; const savedSearchName = 'Saved search for dashboard'; @@ -165,9 +166,14 @@ export default function ({ getService, getPageObjects }) { expect(reportingMenuItemExists).to.be(false); }); - it('does not show the sharing menu item', async () => { + it('shows the sharing menu item', async () => { const shareMenuItemExists = await testSubjects.exists('shareTopNavButton'); - expect(shareMenuItemExists).to.be(false); + expect(shareMenuItemExists).to.be(true); + }); + + it(`Permalinks doesn't show create short-url button`, async () => { + await PageObjects.share.openShareMenuItem('Permalinks'); + await PageObjects.share.createShortUrlMissingOrFail(); }); it('does not show the visualization edit icon', async () => { diff --git a/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts b/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts new file mode 100644 index 000000000000..d564b6809059 --- /dev/null +++ b/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts @@ -0,0 +1,205 @@ +/* + * 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 { SecurityService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const security: SecurityService = getService('security'); + const PageObjects = getPageObjects(['common', 'console', 'security']); + const appsMenu = getService('appsMenu'); + const testSubjects = getService('testSubjects'); + const grokDebugger = getService('grokDebugger'); + + describe('security', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + + // ensure we're logged out so we can login as the appropriate users + await PageObjects.security.forceLogout(); + }); + + after(async () => { + // logout, so the other tests don't accidentally run as the custom users we're testing below + await PageObjects.security.forceLogout(); + }); + + describe('global dev_tools all privileges', () => { + before(async () => { + await security.role.create('global_dev_tools_all_role', { + kibana: [ + { + feature: { + dev_tools: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_dev_tools_all_user', { + password: 'global_dev_tools_all_user-password', + roles: ['global_dev_tools_all_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'global_dev_tools_all_user', + 'global_dev_tools_all_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('global_dev_tools_all_role'); + await security.user.delete('global_dev_tools_all_user'); + }); + + it('shows Dev Tools navlink', async () => { + const navLinks = await appsMenu.readLinks(); + expect(navLinks.map((link: Record) => link.text)).to.eql([ + 'Dev Tools', + 'Management', + ]); + }); + + it(`can navigate to console`, async () => { + await PageObjects.common.navigateToApp('console'); + await testSubjects.existOrFail('console'); + }); + + it(`can navigate to search profiler`, async () => { + await PageObjects.common.navigateToApp('searchProfiler'); + await testSubjects.existOrFail('searchProfiler'); + }); + + it(`can navigate to grok debugger`, async () => { + await PageObjects.common.navigateToApp('grokDebugger'); + await grokDebugger.assertExists(); + }); + }); + + describe('global dev_tools read-only privileges', () => { + before(async () => { + await security.role.create('global_dev_tools_read_role', { + kibana: [ + { + feature: { + dev_tools: ['read'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_dev_tools_read_user', { + password: 'global_dev_tools_read_user-password', + roles: ['global_dev_tools_read_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'global_dev_tools_read_user', + 'global_dev_tools_read_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('global_dev_tools_read_role'); + await security.user.delete('global_dev_tools_read_user'); + }); + + it(`shows 'Dev Tools' navlink`, async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Dev Tools', 'Management']); + }); + + it(`can navigate to console`, async () => { + await PageObjects.common.navigateToApp('console'); + await testSubjects.existOrFail('console'); + }); + + it(`can navigate to search profiler`, async () => { + await PageObjects.common.navigateToApp('searchProfiler'); + await testSubjects.existOrFail('searchProfiler'); + }); + + it(`can navigate to grok debugger`, async () => { + await PageObjects.common.navigateToApp('grokDebugger'); + await grokDebugger.assertExists(); + }); + }); + + describe('no dev_tools privileges', () => { + before(async () => { + await security.role.create('no_dev_tools_privileges_role', { + kibana: [ + { + feature: { + discover: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('no_dev_tools_privileges_user', { + password: 'no_dev_tools_privileges_user-password', + roles: ['no_dev_tools_privileges_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'no_dev_tools_privileges_user', + 'no_dev_tools_privileges_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('no_dev_tools_privileges_role'); + await security.user.delete('no_dev_tools_privileges_user'); + }); + + it(`doesn't show 'Dev Tools' navLink`, async () => { + const navLinks = await appsMenu.readLinks(); + expect(navLinks.map((navLink: any) => navLink.text)).to.not.contain(['Dev Tools']); + }); + + it(`navigating to console redirect to homepage`, async () => { + await PageObjects.common.navigateToUrl('console', '', { + ensureCurrentUrl: false, + }); + await testSubjects.existOrFail('homeApp', 10000); + }); + + it(`navigating to search profiler redirect to homepage`, async () => { + await PageObjects.common.navigateToUrl('searchProfiler', '', { + ensureCurrentUrl: false, + }); + await testSubjects.existOrFail('homeApp', 10000); + }); + + it(`navigating to grok debugger redirect to homepage`, async () => { + await PageObjects.common.navigateToUrl('grokDebugger', '', { + ensureCurrentUrl: false, + }); + await testSubjects.existOrFail('homeApp', 10000); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_spaces.ts b/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_spaces.ts new file mode 100644 index 000000000000..c4a16c18d3be --- /dev/null +++ b/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_spaces.ts @@ -0,0 +1,112 @@ +/* + * 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 { SpacesService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const spacesService: SpacesService = getService('spaces'); + const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'spaceSelector']); + const appsMenu = getService('appsMenu'); + const testSubjects = getService('testSubjects'); + const grokDebugger = getService('grokDebugger'); + + describe('spaces', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + }); + + describe('space with no features disabled', () => { + before(async () => { + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: [], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + }); + + it(`shows 'Dev Tools' navlink`, async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.contain('Dev Tools'); + }); + + it(`can navigate to console`, async () => { + await PageObjects.common.navigateToApp('console'); + await testSubjects.existOrFail('console'); + }); + + it(`can navigate to search profiler`, async () => { + await PageObjects.common.navigateToApp('searchProfiler'); + await testSubjects.existOrFail('searchProfiler'); + }); + + it(`can navigate to grok debugger`, async () => { + await PageObjects.common.navigateToApp('grokDebugger'); + await grokDebugger.assertExists(); + }); + }); + + describe('space with dev_tools disabled', () => { + before(async () => { + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: ['dev_tools'], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + }); + + it(`doesn't show 'Dev Tools' navlink`, async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).not.to.contain('Dev Tools'); + }); + + it(`navigating to console redirect to homepage`, async () => { + await PageObjects.common.navigateToUrl('console', '', { + ensureCurrentUrl: false, + }); + await testSubjects.existOrFail('homeApp', 10000); + }); + + it(`navigating to search profiler redirect to homepage`, async () => { + await PageObjects.common.navigateToUrl('searchProfiler', '', { + ensureCurrentUrl: false, + }); + await testSubjects.existOrFail('homeApp', 10000); + }); + + it(`navigating to grok debugger redirect to homepage`, async () => { + await PageObjects.common.navigateToUrl('grokDebugger', '', { + ensureCurrentUrl: false, + }); + await testSubjects.existOrFail('homeApp', 10000); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/dev_tools/feature_controls/index.ts b/x-pack/test/functional/apps/dev_tools/feature_controls/index.ts new file mode 100644 index 000000000000..d1c53dbc9c43 --- /dev/null +++ b/x-pack/test/functional/apps/dev_tools/feature_controls/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { + describe('feature controls', () => { + loadTestFile(require.resolve('./dev_tools_security')); + loadTestFile(require.resolve('./dev_tools_spaces')); + }); +} diff --git a/x-pack/test/functional/apps/dev_tools/index.ts b/x-pack/test/functional/apps/dev_tools/index.ts new file mode 100644 index 000000000000..04135bf27a54 --- /dev/null +++ b/x-pack/test/functional/apps/dev_tools/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { + describe('console', function() { + this.tags('ciGroup3'); + + loadTestFile(require.resolve('./feature_controls')); + }); +} diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts new file mode 100644 index 000000000000..0afb925ee46f --- /dev/null +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts @@ -0,0 +1,253 @@ +/* + * 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 { SecurityService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const security: SecurityService = getService('security'); + const PageObjects = getPageObjects([ + 'common', + 'discover', + 'timePicker', + 'security', + 'share', + 'spaceSelector', + ]); + const testSubjects = getService('testSubjects'); + const appsMenu = getService('appsMenu'); + + async function setDiscoverTimeRange() { + const fromTime = '2015-09-19 06:31:44.000'; + const toTime = '2015-09-23 18:31:44.000'; + await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + } + + describe('security', () => { + before(async () => { + await esArchiver.load('discover/feature_controls/security'); + await esArchiver.loadIfNeeded('logstash_functional'); + + // ensure we're logged out so we can login as the appropriate users + await PageObjects.security.logout(); + }); + + after(async () => { + await esArchiver.unload('discover/feature_controls/security'); + + // logout, so the other tests don't accidentally run as the custom users we're testing below + await PageObjects.security.logout(); + }); + + describe('global discover all privileges', () => { + before(async () => { + await security.role.create('global_discover_all_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + discover: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_discover_all_user', { + password: 'global_discover_all_user-password', + roles: ['global_discover_all_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'global_discover_all_user', + 'global_discover_all_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('global_discover_all_role'); + await security.user.delete('global_discover_all_user'); + }); + + it('shows discover navlink', async () => { + const navLinks = await appsMenu.readLinks(); + expect(navLinks.map((link: Record) => link.text)).to.eql([ + 'Discover', + 'Management', + ]); + }); + + it('shows save button', async () => { + await PageObjects.common.navigateToApp('discover'); + await testSubjects.existOrFail('discoverSaveButton', 20000); + }); + + it('Permalinks shows create short-url button', async () => { + await PageObjects.share.openShareMenuItem('Permalinks'); + await PageObjects.share.createShortUrlExistOrFail(); + }); + }); + + describe('global discover read-only privileges', () => { + before(async () => { + await security.role.create('global_discover_read_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + discover: ['read'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_discover_read_user', { + password: 'global_discover_read_user-password', + roles: ['global_discover_read_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'global_discover_read_user', + 'global_discover_read_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('global_discover_read_role'); + await security.user.delete('global_discover_read_user'); + }); + + it('shows discover navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Discover', 'Management']); + }); + + it(`doesn't show save button`, async () => { + await PageObjects.common.navigateToApp('discover'); + await testSubjects.existOrFail('discoverNewButton', 10000); + await testSubjects.missingOrFail('discoverSaveButton'); + }); + + it(`doesn't show visualize button`, async () => { + await PageObjects.common.navigateToApp('discover'); + await setDiscoverTimeRange(); + await PageObjects.discover.clickFieldListItem('bytes'); + await PageObjects.discover.expectMissingFieldListItemVisualize('bytes'); + }); + + it(`Permalinks doesn't show create short-url button`, async () => { + await PageObjects.share.openShareMenuItem('Permalinks'); + await PageObjects.share.createShortUrlMissingOrFail(); + }); + }); + + describe('discover and visualize privileges', () => { + before(async () => { + await security.role.create('global_discover_visualize_read_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + discover: ['read'], + visualize: ['read'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_discover_visualize_read_user', { + password: 'global_discover_visualize_read_user-password', + roles: ['global_discover_visualize_read_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'global_discover_visualize_read_user', + 'global_discover_visualize_read_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('global_discover_visualize_read_role'); + await security.user.delete('global_discover_visualize_read_user'); + }); + + it(`shows the visualize button`, async () => { + await PageObjects.common.navigateToApp('discover'); + await setDiscoverTimeRange(); + await PageObjects.discover.clickFieldListItem('bytes'); + await PageObjects.discover.expectFieldListItemVisualize('bytes'); + }); + }); + + describe('no discover privileges', () => { + before(async () => { + await security.role.create('no_discover_privileges_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + dashboard: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('no_discover_privileges_user', { + password: 'no_discover_privileges_user-password', + roles: ['no_discover_privileges_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'no_discover_privileges_user', + 'no_discover_privileges_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('no_discover_privileges_role'); + await security.user.delete('no_discover_privileges_user'); + }); + + it(`redirects to the home page`, async () => { + await PageObjects.common.navigateToUrl('discover', '', { + ensureCurrentUrl: false, + }); + await testSubjects.existOrFail('homeApp', 10000); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts new file mode 100644 index 000000000000..7fffbcfd1034 --- /dev/null +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts @@ -0,0 +1,147 @@ +/* + * 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 { SpacesService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const spacesService: SpacesService = getService('spaces'); + const PageObjects = getPageObjects([ + 'common', + 'discover', + 'timePicker', + 'security', + 'spaceSelector', + ]); + const testSubjects = getService('testSubjects'); + const appsMenu = getService('appsMenu'); + + async function setDiscoverTimeRange() { + const fromTime = '2015-09-19 06:31:44.000'; + const toTime = '2015-09-23 18:31:44.000'; + await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + } + + describe('spaces', () => { + before(async () => { + await esArchiver.loadIfNeeded('logstash_functional'); + }); + + describe('space with no features disabled', () => { + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('discover/feature_controls/spaces'); + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: [], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('discover/feature_controls/spaces'); + }); + + it('shows discover navlink', async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.contain('Discover'); + }); + + it('shows save button', async () => { + await PageObjects.common.navigateToApp('discover', { + basePath: '/s/custom_space', + }); + await testSubjects.existOrFail('discoverSaveButton', 10000); + }); + + it('shows "visualize" field button', async () => { + await PageObjects.common.navigateToApp('discover', { + basePath: '/s/custom_space', + }); + await setDiscoverTimeRange(); + await PageObjects.discover.clickFieldListItem('bytes'); + await PageObjects.discover.expectFieldListItemVisualize('bytes'); + }); + }); + + describe('space with Discover disabled', () => { + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('discover/feature_controls/spaces'); + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: ['discover'], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('discover/feature_controls/spaces'); + }); + + it(`doesn't show discover navlink`, async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).not.to.contain('Discover'); + }); + + it(`redirects to the home page`, async () => { + // to test whether they're being redirected properly, we first load + // the discover app in the default space, and then we load up the discover + // app in the custom space and ensure we end up on the home page + await PageObjects.common.navigateToApp('discover'); + await PageObjects.common.navigateToUrl('discover', '', { + basePath: '/s/custom_space', + shouldLoginIfPrompted: false, + ensureCurrentUrl: false, + }); + await PageObjects.spaceSelector.expectHomePage('custom_space'); + }); + }); + + describe('space with Visualize disabled', async () => { + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('spaces/disabled_features'); + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: ['visualize'], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('spaces/disabled_features'); + }); + + it('Does not show the "visualize" field button', async () => { + await PageObjects.common.navigateToApp('discover', { + basePath: '/s/custom_space', + }); + await setDiscoverTimeRange(); + await PageObjects.discover.clickFieldListItem('bytes'); + await PageObjects.discover.expectMissingFieldListItemVisualize('bytes'); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/discover/feature_controls/index.ts b/x-pack/test/functional/apps/discover/feature_controls/index.ts new file mode 100644 index 000000000000..1f822c8c7998 --- /dev/null +++ b/x-pack/test/functional/apps/discover/feature_controls/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { + describe('feature controls', () => { + loadTestFile(require.resolve('./discover_security')); + loadTestFile(require.resolve('./discover_spaces')); + }); +} diff --git a/x-pack/test/functional/apps/discover/index.ts b/x-pack/test/functional/apps/discover/index.ts new file mode 100644 index 000000000000..575d485e6115 --- /dev/null +++ b/x-pack/test/functional/apps/discover/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { + describe('discover', function() { + this.tags('ciGroup3'); + + loadTestFile(require.resolve('./feature_controls')); + }); +} diff --git a/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts b/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts new file mode 100644 index 000000000000..4adb27bb3b18 --- /dev/null +++ b/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts @@ -0,0 +1,194 @@ +/* + * 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 { SecurityService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const security: SecurityService = getService('security'); + const PageObjects = getPageObjects(['common', 'graph', 'security', 'error']); + const testSubjects = getService('testSubjects'); + const appsMenu = getService('appsMenu'); + + describe('security', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + // ensure we're logged out so we can login as the appropriate users + await PageObjects.security.forceLogout(); + }); + + after(async () => { + // logout, so the other tests don't accidentally run as the custom users we're testing below + await PageObjects.security.forceLogout(); + }); + + describe('global graph all privileges', () => { + before(async () => { + await security.role.create('global_graph_all_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + graph: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_graph_all_user', { + password: 'global_graph_all_user-password', + roles: ['global_graph_all_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'global_graph_all_user', + 'global_graph_all_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('global_graph_all_role'); + await security.user.delete('global_graph_all_user'); + }); + + it('shows graph navlink', async () => { + const navLinks = await appsMenu.readLinks(); + expect(navLinks.map((link: Record) => link.text)).to.eql([ + 'Graph', + 'Management', + ]); + }); + + it('shows save button', async () => { + await PageObjects.common.navigateToApp('graph'); + await testSubjects.existOrFail('graphSaveButton'); + }); + + it('shows delete button', async () => { + await PageObjects.common.navigateToApp('graph'); + await testSubjects.existOrFail('graphDeleteButton'); + }); + }); + + describe('global graph read-only privileges', () => { + before(async () => { + await security.role.create('global_graph_read_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + graph: ['read'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_graph_read_user', { + password: 'global_graph_read_user-password', + roles: ['global_graph_read_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'global_graph_read_user', + 'global_graph_read_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('global_graph_read_role'); + await security.user.delete('global_graph_read_user'); + }); + + it('shows graph navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Graph', 'Management']); + }); + + it(`doesn't show save button`, async () => { + await PageObjects.common.navigateToApp('graph'); + await testSubjects.existOrFail('graphOpenButton'); + await testSubjects.missingOrFail('graphSaveButton'); + }); + + it(`doesn't show delete button`, async () => { + await PageObjects.common.navigateToApp('graph'); + await testSubjects.existOrFail('graphOpenButton'); + await testSubjects.missingOrFail('graphDeleteButton'); + }); + }); + + describe('no graph privileges', () => { + before(async () => { + await security.role.create('no_graph_privileges_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + dashboard: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('no_graph_privileges_user', { + password: 'no_graph_privileges_user-password', + roles: ['no_graph_privileges_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'no_graph_privileges_user', + 'no_graph_privileges_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('no_graph_privileges_role'); + await security.user.delete('no_graph_privileges_user'); + }); + + it(`doesn't show graph navlink`, async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).not.to.contain('Graph'); + }); + + it(`navigating to app displays a 404`, async () => { + await PageObjects.common.navigateToUrl('graph', '', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + + await PageObjects.error.expectNotFound(); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/graph/feature_controls/graph_spaces.ts b/x-pack/test/functional/apps/graph/feature_controls/graph_spaces.ts new file mode 100644 index 000000000000..976286fefc39 --- /dev/null +++ b/x-pack/test/functional/apps/graph/feature_controls/graph_spaces.ts @@ -0,0 +1,93 @@ +/* + * 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 { SpacesService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const spacesService: SpacesService = getService('spaces'); + const PageObjects = getPageObjects(['common', 'graph', 'security', 'error']); + const testSubjects = getService('testSubjects'); + const appsMenu = getService('appsMenu'); + + describe('spaces', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + }); + describe('space with no features disabled', () => { + before(async () => { + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: [], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + }); + + it('shows graph navlink', async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.contain('Graph'); + }); + + it('shows save button', async () => { + await PageObjects.common.navigateToApp('graph', { + basePath: '/s/custom_space', + }); + await testSubjects.existOrFail('graphSaveButton'); + }); + + it('shows delete button', async () => { + await PageObjects.common.navigateToApp('graph', { + basePath: '/s/custom_space', + }); + await testSubjects.existOrFail('graphDeleteButton'); + }); + }); + + describe('space with Graph disabled', () => { + before(async () => { + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: ['graph'], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + }); + + it(`doesn't show graph navlink`, async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).not.to.contain('Graph'); + }); + + it(`navigating to app shows 404`, async () => { + await PageObjects.common.navigateToUrl('graph', '', { + basePath: '/s/custom_space', + shouldLoginIfPrompted: false, + ensureCurrentUrl: false, + }); + await PageObjects.error.expectNotFound(); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/graph/feature_controls/index.ts b/x-pack/test/functional/apps/graph/feature_controls/index.ts new file mode 100644 index 000000000000..95efaf58b969 --- /dev/null +++ b/x-pack/test/functional/apps/graph/feature_controls/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { + describe('feature controls', () => { + loadTestFile(require.resolve('./graph_security')); + loadTestFile(require.resolve('./graph_spaces')); + }); +} diff --git a/x-pack/test/functional/apps/graph/index.js b/x-pack/test/functional/apps/graph/index.js index 98b360320c2c..1eafe341ba2d 100644 --- a/x-pack/test/functional/apps/graph/index.js +++ b/x-pack/test/functional/apps/graph/index.js @@ -8,6 +8,7 @@ export default function ({ loadTestFile }) { describe('graph app', function () { this.tags('ciGroup1'); + loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./graph')); }); } diff --git a/x-pack/test/functional/apps/index_patterns/feature_controls/index.ts b/x-pack/test/functional/apps/index_patterns/feature_controls/index.ts new file mode 100644 index 000000000000..2d0398173153 --- /dev/null +++ b/x-pack/test/functional/apps/index_patterns/feature_controls/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { + describe('feature controls', () => { + loadTestFile(require.resolve('./index_patterns_security')); + loadTestFile(require.resolve('./index_patterns_spaces')); + }); +} diff --git a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts new file mode 100644 index 000000000000..dccbde316a1f --- /dev/null +++ b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts @@ -0,0 +1,195 @@ +/* + * 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 { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'settings', 'security']); + const appsMenu = getService('appsMenu'); + const testSubjects = getService('testSubjects'); + + describe('security', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + }); + + describe('global index_patterns all privileges', () => { + before(async () => { + await security.role.create('global_index_patterns_all_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + indexPatterns: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_index_patterns_all_user', { + password: 'global_index_patterns_all_user-password', + roles: ['global_index_patterns_all_role'], + full_name: 'test user', + }); + + await PageObjects.security.logout(); + + await PageObjects.security.login( + 'global_index_patterns_all_user', + 'global_index_patterns_all_user-password', + { + expectSpaceSelector: false, + } + ); + + await kibanaServer.uiSettings.replace({}); + await PageObjects.settings.navigateTo(); + }); + + after(async () => { + await Promise.all([ + security.role.delete('global_index_patterns_all_role'), + security.user.delete('global_index_patterns_all_user'), + PageObjects.security.logout(), + ]); + }); + + it('shows management navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Management']); + }); + + it(`index pattern listing shows create button`, async () => { + await PageObjects.settings.clickKibanaIndexPatterns(); + await testSubjects.existOrFail('createIndexPatternButton'); + }); + }); + + describe('global index_patterns read-only privileges', () => { + before(async () => { + await security.role.create('global_index_patterns_read_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + indexPatterns: ['read'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_index_patterns_read_user', { + password: 'global_index_patterns_read_user-password', + roles: ['global_index_patterns_read_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'global_index_patterns_read_user', + 'global_index_patterns_read_user-password', + { + expectSpaceSelector: false, + } + ); + + await kibanaServer.uiSettings.replace({}); + await PageObjects.settings.navigateTo(); + }); + + after(async () => { + await security.role.delete('global_index_patterns_read_role'); + await security.user.delete('global_index_patterns_read_user'); + }); + + it('shows management navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Management']); + }); + + it(`index pattern listing doesn't show create button`, async () => { + await PageObjects.settings.clickKibanaIndexPatterns(); + await testSubjects.existOrFail('indexPatternTable'); + await testSubjects.missingOrFail('createIndexPatternButton'); + }); + }); + + describe('no index_patterns privileges', () => { + before(async () => { + await security.role.create('no_index_patterns_privileges_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + discover: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('no_index_patterns_privileges_user', { + password: 'no_index_patterns_privileges_user-password', + roles: ['no_index_patterns_privileges_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'no_index_patterns_privileges_user', + 'no_index_patterns_privileges_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('no_index_patterns_privileges_role'); + await security.user.delete('no_index_patterns_privileges_user'); + }); + + it('shows Management navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Discover', 'Management']); + }); + + it(`doesn't show Index Patterns in management side-nav`, async () => { + await PageObjects.settings.navigateTo(); + await testSubjects.existOrFail('kibana'); + await testSubjects.missingOrFail('index_patterns'); + }); + + it(`does not allow navigation to Index Patterns; redirects to Kibana home`, async () => { + await PageObjects.common.navigateToActualUrl('kibana', 'management/kibana/index_patterns', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('homeApp', 10000); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.ts b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.ts new file mode 100644 index 000000000000..9a3bd5694f76 --- /dev/null +++ b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.ts @@ -0,0 +1,85 @@ +/* + * 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 { SpacesService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const spacesService: SpacesService = getService('spaces'); + const PageObjects = getPageObjects(['common', 'settings', 'security']); + const testSubjects = getService('testSubjects'); + const appsMenu = getService('appsMenu'); + + describe('spaces', () => { + before(async () => { + await esArchiver.loadIfNeeded('logstash_functional'); + }); + + describe('space with no features disabled', () => { + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('empty_kibana'); + + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: [], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('empty_kibana'); + }); + + it('shows Management navlink', async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.contain('Management'); + }); + + it(`index pattern listing shows create button`, async () => { + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaIndexPatterns(); + await testSubjects.existOrFail('createIndexPatternButton'); + }); + }); + + describe('space with Index Patterns disabled', () => { + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('empty_kibana'); + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: ['indexPatterns'], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('empty_kibana'); + }); + + it(`redirects to Kibana home`, async () => { + await PageObjects.common.navigateToActualUrl('kibana', 'management/kibana/index_patterns', { + basePath: `/s/custom_space`, + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('homeApp', 10000); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/index_patterns/index.ts b/x-pack/test/functional/apps/index_patterns/index.ts new file mode 100644 index 000000000000..ac5479e9c1c1 --- /dev/null +++ b/x-pack/test/functional/apps/index_patterns/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function advancedSettingsApp({ + loadTestFile, +}: KibanaFunctionalTestDefaultProviders) { + describe('Index Patterns', function indexPatternsTestSuite() { + this.tags('ciGroup2'); + loadTestFile(require.resolve('./feature_controls')); + }); +} diff --git a/x-pack/test/functional/apps/infra/feature_controls/index.ts b/x-pack/test/functional/apps/infra/feature_controls/index.ts new file mode 100644 index 000000000000..269e2b037cf1 --- /dev/null +++ b/x-pack/test/functional/apps/infra/feature_controls/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { + describe('feature controls', () => { + loadTestFile(require.resolve('./infrastructure_security')); + loadTestFile(require.resolve('./infrastructure_spaces')); + loadTestFile(require.resolve('./logs_security')); + loadTestFile(require.resolve('./logs_spaces')); + }); +} diff --git a/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts new file mode 100644 index 000000000000..a8a3cec823e7 --- /dev/null +++ b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts @@ -0,0 +1,448 @@ +/* + * 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 { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; +import { DATES } from '../constants'; + +const DATE_WITH_DATA = new Date(DATES.metricsAndLogs.hosts.withData); +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'infraHome', 'security']); + const testSubjects = getService('testSubjects'); + const appsMenu = getService('appsMenu'); + + describe('infrastructure security', () => { + describe('global infrastructure all privileges', () => { + before(async () => { + await security.role.create('global_infrastructure_all_role', { + elasticsearch: { + indices: [{ names: ['metricbeat-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + infrastructure: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_infrastructure_all_user', { + password: 'global_infrastructure_all_user-password', + roles: ['global_infrastructure_all_role'], + full_name: 'test user', + }); + + await PageObjects.security.forceLogout(); + + await PageObjects.security.login( + 'global_infrastructure_all_user', + 'global_infrastructure_all_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await Promise.all([ + security.role.delete('global_infrastructure_all_role'), + security.user.delete('global_infrastructure_all_user'), + PageObjects.security.forceLogout(), + ]); + }); + + it('shows infrastructure navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Infrastructure', 'Management']); + }); + + describe('infrastructure landing page without data', () => { + it(`shows 'Change source configuration' button`, async () => { + await PageObjects.common.navigateToActualUrl('infraOps', 'home', { + ensureCurrentUrl: true, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('infrastructureViewSetupInstructionsButton'); + await testSubjects.existOrFail('configureSourceButton'); + }); + }); + + describe('infrastructure landing page with data', () => { + before(async () => { + await esArchiver.load('infra/metrics_and_logs'); + }); + + after(async () => { + await esArchiver.unload('infra/metrics_and_logs'); + }); + + it(`shows Wafflemap`, async () => { + await PageObjects.common.navigateToActualUrl('infraOps', 'home', { + ensureCurrentUrl: true, + shouldLoginIfPrompted: false, + }); + await PageObjects.infraHome.goToTime(DATE_WITH_DATA); + await testSubjects.existOrFail('waffleMap'); + }); + + describe('context menu', () => { + before(async () => { + await testSubjects.click('nodeContainer'); + }); + + it(`does not show link to view logs`, async () => { + await testSubjects.missingOrFail('viewLogsContextMenuItem'); + }); + + it(`does not show link to view apm traces`, async () => { + await testSubjects.missingOrFail('viewApmTracesContextMenuItem'); + }); + }); + }); + + it(`metrics page is visible`, async () => { + await PageObjects.common.navigateToActualUrl( + 'infraOps', + '/metrics/host/demo-stack-redis-01', + { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + } + ); + await testSubjects.existOrFail('infraMetricsPage'); + }); + }); + + describe('global infrastructure read privileges', () => { + before(async () => { + await security.role.create('global_infrastructure_read_role', { + elasticsearch: { + indices: [{ names: ['metricbeat-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + infrastructure: ['read'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_infrastructure_read_user', { + password: 'global_infrastructure_read_user-password', + roles: ['global_infrastructure_read_role'], + full_name: 'test user', + }); + + await PageObjects.security.forceLogout(); + + await PageObjects.security.login( + 'global_infrastructure_read_user', + 'global_infrastructure_read_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await Promise.all([ + security.role.delete('global_infrastructure_read_role'), + security.user.delete('global_infrastructure_read_user'), + PageObjects.security.forceLogout(), + ]); + }); + + it('shows infrastructure navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Infrastructure', 'Management']); + }); + + describe('infrastructure landing page without data', () => { + it(`doesn't show 'Change source configuration' button`, async () => { + await PageObjects.common.navigateToActualUrl('infraOps', 'home', { + ensureCurrentUrl: true, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('infrastructureViewSetupInstructionsButton'); + await testSubjects.missingOrFail('configureSourceButton'); + }); + }); + + describe('infrastructure landing page with data', () => { + before(async () => { + await esArchiver.load('infra/metrics_and_logs'); + }); + + after(async () => { + await esArchiver.unload('infra/metrics_and_logs'); + }); + + it(`shows Wafflemap`, async () => { + await PageObjects.common.navigateToActualUrl('infraOps', 'home', { + ensureCurrentUrl: true, + shouldLoginIfPrompted: false, + }); + await PageObjects.infraHome.goToTime(DATE_WITH_DATA); + await testSubjects.existOrFail('waffleMap'); + }); + + describe('context menu', () => { + before(async () => { + await testSubjects.click('nodeContainer'); + }); + + it(`does not show link to view logs`, async () => { + await testSubjects.missingOrFail('viewLogsContextMenuItem'); + }); + + it(`does not show link to view apm traces`, async () => { + await testSubjects.missingOrFail('viewApmTracesContextMenuItem'); + }); + }); + }); + + it(`metrics page is visible`, async () => { + await PageObjects.common.navigateToActualUrl( + 'infraOps', + '/metrics/host/demo-stack-redis-01', + { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + } + ); + await testSubjects.existOrFail('infraMetricsPage'); + }); + }); + + describe('global infrastructure read & logs read privileges', () => { + before(async () => { + await security.role.create('global_infrastructure_logs_read_role', { + elasticsearch: { + indices: [ + { + names: ['metricbeat-*', 'filebeat-*'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + kibana: [ + { + feature: { + infrastructure: ['read'], + logs: ['read'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_infrastructure_logs_read_user', { + password: 'global_infrastructure_logs_read_user-password', + roles: ['global_infrastructure_logs_read_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'global_infrastructure_logs_read_user', + 'global_infrastructure_logs_read_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('global_infrastructure_logs_read_role'); + await security.user.delete('global_infrastructure_logs_read_user'); + }); + + describe('infrastructure landing page with data', () => { + before(async () => { + await esArchiver.load('infra/metrics_and_logs'); + }); + + after(async () => { + await esArchiver.unload('infra/metrics_and_logs'); + }); + + it(`context menu allows user to view logs`, async () => { + await PageObjects.common.navigateToActualUrl('infraOps', 'home', { + ensureCurrentUrl: true, + shouldLoginIfPrompted: false, + }); + await PageObjects.infraHome.goToTime(DATE_WITH_DATA); + await testSubjects.existOrFail('waffleMap'); + await testSubjects.click('nodeContainer'); + await testSubjects.click('viewLogsContextMenuItem'); + await testSubjects.existOrFail('infraLogsPage'); + }); + }); + }); + + describe('global infrastructure read & apm privileges', () => { + before(async () => { + await security.role.create('global_infrastructure_apm_read_role', { + elasticsearch: { + indices: [ + { + names: ['metricbeat-*', 'filebeat-*'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + kibana: [ + { + feature: { + infrastructure: ['read'], + apm: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_infrastructure_apm_read_user', { + password: 'global_infrastructure_apm_read_user-password', + roles: ['global_infrastructure_apm_read_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'global_infrastructure_apm_read_user', + 'global_infrastructure_apm_read_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('global_infrastructure_apm_read_role'); + await security.user.delete('global_infrastructure_apm_read_user'); + }); + + describe('infrastructure landing page with data', () => { + before(async () => { + await esArchiver.load('infra/metrics_and_logs'); + }); + + after(async () => { + await esArchiver.unload('infra/metrics_and_logs'); + }); + + it(`context menu allows user to view APM traces`, async () => { + await PageObjects.common.navigateToActualUrl('infraOps', 'home', { + ensureCurrentUrl: true, + shouldLoginIfPrompted: false, + }); + await PageObjects.infraHome.goToTime(DATE_WITH_DATA); + await testSubjects.existOrFail('waffleMap'); + await testSubjects.click('nodeContainer'); + await testSubjects.click('viewApmTracesContextMenuItem'); + await testSubjects.existOrFail('apmMainContainer'); + }); + }); + }); + + describe('global infrastructure no privileges', () => { + before(async () => { + await security.role.create('no_infrastructure_privileges_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + logs: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('no_infrastructure_privileges_user', { + password: 'no_infrastructure_privileges_user-password', + roles: ['no_infrastructure_privileges_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'no_infrastructure_privileges_user', + 'no_infrastructure_privileges_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('no_infrastructure_privileges_role'); + await security.user.delete('no_infrastructure_privileges_user'); + }); + + it(`doesn't show infrastructure navlink`, async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.not.contain(['Infrastructure']); + }); + + it(`infrastructure root renders not found page`, async () => { + await PageObjects.common.navigateToActualUrl('infraOps', '', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('infraNotFoundPage'); + }); + + it(`infrastructure home page renders not found page`, async () => { + await PageObjects.common.navigateToActualUrl('infraOps', 'home', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('infraNotFoundPage'); + }); + + it(`infrastructure landing page renders not found page`, async () => { + await PageObjects.common.navigateToActualUrl('infraOps', 'infrastructure', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('infraNotFoundPage'); + }); + + it(`infrastructure snapshot page renders not found page`, async () => { + await PageObjects.common.navigateToActualUrl('infraOps', 'infrastructure/snapshot', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('infraNotFoundPage'); + }); + + it(`metrics page renders not found page`, async () => { + await PageObjects.common.navigateToActualUrl( + 'infraOps', + '/metrics/host/demo-stack-redis-01', + { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + } + ); + await testSubjects.existOrFail('infraNotFoundPage'); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/infra/feature_controls/infrastructure_spaces.ts b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_spaces.ts new file mode 100644 index 000000000000..8e274e49b15b --- /dev/null +++ b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_spaces.ts @@ -0,0 +1,240 @@ +/* + * 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 { SpacesService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; +import { DATES } from '../constants'; + +const DATE_WITH_DATA = new Date(DATES.metricsAndLogs.hosts.withData); + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const spacesService: SpacesService = getService('spaces'); + const PageObjects = getPageObjects(['common', 'infraHome', 'security', 'spaceSelector']); + const testSubjects = getService('testSubjects'); + const appsMenu = getService('appsMenu'); + + describe('infrastructure spaces', () => { + before(async () => { + await esArchiver.load('infra/metrics_and_logs'); + }); + + after(async () => { + await esArchiver.unload('infra/metrics_and_logs'); + }); + + describe('space with no features disabled', () => { + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('empty_kibana'); + + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: [], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('empty_kibana'); + }); + + it('shows Infrastructure navlink', async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.contain('Infrastructure'); + }); + + it(`landing page shows Wafflemap`, async () => { + await PageObjects.common.navigateToActualUrl('infraOps', 'home', { + basePath: '/s/custom_space', + ensureCurrentUrl: true, + }); + await PageObjects.infraHome.goToTime(DATE_WITH_DATA); + await testSubjects.existOrFail('waffleMap'); + }); + + describe('context menu', () => { + before(async () => { + await testSubjects.click('nodeContainer'); + }); + + it(`shows link to view logs`, async () => { + await testSubjects.existOrFail('viewLogsContextMenuItem'); + }); + + it(`shows link to view apm traces`, async () => { + await testSubjects.existOrFail('viewApmTracesContextMenuItem'); + }); + }); + }); + + describe('space with Infrastructure disabled', () => { + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('empty_kibana'); + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: ['infrastructure'], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('empty_kibana'); + }); + + it(`doesn't show infrastructure navlink`, async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).not.to.contain('Infrastructure'); + }); + + it(`infrastructure root renders not found page`, async () => { + await PageObjects.common.navigateToActualUrl('infraOps', '', { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('infraNotFoundPage'); + }); + + it(`infrastructure home page renders not found page`, async () => { + await PageObjects.common.navigateToActualUrl('infraOps', 'home', { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('infraNotFoundPage'); + }); + + it(`infrastructure landing page renders not found page`, async () => { + await PageObjects.common.navigateToActualUrl('infraOps', 'infrastructure', { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('infraNotFoundPage'); + }); + + it(`infrastructure snapshot page renders not found page`, async () => { + await PageObjects.common.navigateToActualUrl('infraOps', 'infrastructure/snapshot', { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('infraNotFoundPage'); + }); + + it(`metrics page renders not found page`, async () => { + await PageObjects.common.navigateToActualUrl( + 'infraOps', + '/metrics/host/demo-stack-redis-01', + { + basePath: '/s/custom_space', + ensureCurrentUrl: true, + } + ); + await testSubjects.existOrFail('infraNotFoundPage'); + }); + }); + + describe('space with Logs disabled', () => { + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('empty_kibana'); + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: ['logs'], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('empty_kibana'); + }); + + it(`landing page shows Wafflemap`, async () => { + await PageObjects.common.navigateToActualUrl('infraOps', 'home', { + basePath: '/s/custom_space', + ensureCurrentUrl: true, + }); + await PageObjects.infraHome.goToTime(DATE_WITH_DATA); + await testSubjects.existOrFail('waffleMap'); + }); + + describe('context menu', () => { + before(async () => { + await testSubjects.click('nodeContainer'); + }); + + it(`doesn't show link to view logs`, async () => { + await testSubjects.missingOrFail('viewLogsContextMenuItem'); + }); + + it(`shows link to view apm traces`, async () => { + await testSubjects.existOrFail('viewApmTracesContextMenuItem'); + }); + }); + }); + + describe('space with APM disabled', () => { + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('empty_kibana'); + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: ['apm'], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('empty_kibana'); + }); + + it(`landing page shows Wafflemap`, async () => { + await PageObjects.common.navigateToActualUrl('infraOps', 'home', { + basePath: '/s/custom_space', + ensureCurrentUrl: true, + }); + await PageObjects.infraHome.goToTime(DATE_WITH_DATA); + await testSubjects.existOrFail('waffleMap'); + }); + + describe('context menu', () => { + before(async () => { + await testSubjects.click('nodeContainer'); + }); + + it(`shows link to view logs`, async () => { + await testSubjects.existOrFail('viewLogsContextMenuItem'); + }); + + it(`doesn't show link to view apm traces`, async () => { + await testSubjects.missingOrFail('viewApmTracesContextMenuItem'); + }); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts b/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts new file mode 100644 index 000000000000..02f2372a86e1 --- /dev/null +++ b/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts @@ -0,0 +1,197 @@ +/* + * 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 { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'infraHome', 'security']); + const testSubjects = getService('testSubjects'); + const appsMenu = getService('appsMenu'); + + describe('logs security', () => { + before(async () => { + esArchiver.load('empty_kibana'); + }); + describe('global logs all privileges', () => { + before(async () => { + await security.role.create('global_logs_all_role', { + elasticsearch: { + indices: [{ names: ['metricbeat-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + logs: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_logs_all_user', { + password: 'global_logs_all_user-password', + roles: ['global_logs_all_role'], + full_name: 'test user', + }); + + await PageObjects.security.forceLogout(); + + await PageObjects.security.login('global_logs_all_user', 'global_logs_all_user-password', { + expectSpaceSelector: false, + }); + }); + + after(async () => { + await Promise.all([ + security.role.delete('global_logs_all_role'), + security.user.delete('global_logs_all_user'), + PageObjects.security.forceLogout(), + ]); + }); + + it('shows logs navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Logs', 'Management']); + }); + + describe('logs landing page without data', () => { + it(`shows 'Change source configuration' button`, async () => { + await PageObjects.common.navigateToActualUrl('infraOps', 'logs', { + ensureCurrentUrl: true, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('infraLogsPage'); + await testSubjects.existOrFail('logsViewSetupInstructionsButton'); + await testSubjects.existOrFail('configureSourceButton'); + }); + }); + }); + + describe('global logs read privileges', () => { + before(async () => { + await security.role.create('global_logs_read_role', { + elasticsearch: { + indices: [{ names: ['metricbeat-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + logs: ['read'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_logs_read_user', { + password: 'global_logs_read_user-password', + roles: ['global_logs_read_role'], + full_name: 'test user', + }); + + await PageObjects.security.forceLogout(); + + await PageObjects.security.login( + 'global_logs_read_user', + 'global_logs_read_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await Promise.all([ + security.role.delete('global_logs_read_role'), + security.user.delete('global_logs_read_user'), + PageObjects.security.forceLogout(), + ]); + }); + + it('shows logs navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Logs', 'Management']); + }); + + describe('logs landing page without data', () => { + it(`doesn't show 'Change source configuration' button`, async () => { + await PageObjects.common.navigateToActualUrl('infraOps', 'logs', { + ensureCurrentUrl: true, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('infraLogsPage'); + await testSubjects.existOrFail('logsViewSetupInstructionsButton'); + await testSubjects.missingOrFail('configureSourceButton'); + }); + }); + }); + + describe('global logs no privileges', () => { + before(async () => { + await security.role.create('global_logs_no_privileges_role', { + elasticsearch: { + indices: [{ names: ['metricbeat-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + infrastructure: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_logs_no_privileges_user', { + password: 'global_logs_no_privileges_user-password', + roles: ['global_logs_no_privileges_role'], + full_name: 'test user', + }); + + await PageObjects.security.forceLogout(); + + await PageObjects.security.login( + 'global_logs_no_privileges_user', + 'global_logs_no_privileges_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await Promise.all([ + security.role.delete('global_logs_no_privileges_role'), + security.user.delete('global_logs_no_privileges_user'), + PageObjects.security.forceLogout(), + ]); + }); + + it(`doesn't show logs navlink`, async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.not.contain('Logs'); + }); + + it('logs landing page renders not found page', async () => { + await PageObjects.common.navigateToActualUrl('infraOps', 'logs', { + ensureCurrentUrl: true, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('infraNotFoundPage'); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/infra/feature_controls/logs_spaces.ts b/x-pack/test/functional/apps/infra/feature_controls/logs_spaces.ts new file mode 100644 index 000000000000..5cd94516e1ad --- /dev/null +++ b/x-pack/test/functional/apps/infra/feature_controls/logs_spaces.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { SpacesService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const spacesService: SpacesService = getService('spaces'); + const PageObjects = getPageObjects(['common', 'infraHome', 'security', 'spaceSelector']); + const testSubjects = getService('testSubjects'); + const appsMenu = getService('appsMenu'); + + describe('logs spaces', () => { + describe('space with no features disabled', () => { + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('empty_kibana'); + + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: [], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('empty_kibana'); + }); + + it('shows Logs navlink', async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.contain('Logs'); + }); + + describe('logs landing page without data', () => { + it(`shows 'Change source configuration' button`, async () => { + await PageObjects.common.navigateToActualUrl('infraOps', 'logs', { + basePath: '/s/custom_space', + ensureCurrentUrl: true, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('infraLogsPage'); + await testSubjects.existOrFail('logsViewSetupInstructionsButton'); + await testSubjects.existOrFail('configureSourceButton'); + }); + }); + }); + + describe('space with Logs disabled', () => { + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('empty_kibana'); + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: ['logs'], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('empty_kibana'); + }); + + it(`doesn't show Logs navlink`, async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.not.contain('Logs'); + }); + + it('logs landing page renders not found page', async () => { + await PageObjects.common.navigateToActualUrl('infraOps', 'logs', { + basePath: '/s/custom_space', + ensureCurrentUrl: true, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('infraNotFoundPage'); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/infra/index.ts b/x-pack/test/functional/apps/infra/index.ts index 922e0a48f513..d2f293fa8343 100644 --- a/x-pack/test/functional/apps/infra/index.ts +++ b/x-pack/test/functional/apps/infra/index.ts @@ -12,6 +12,7 @@ export default ({ loadTestFile }: KibanaFunctionalTestDefaultProviders) => { this.tags('ciGroup7'); loadTestFile(require.resolve('./home_page')); + loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./logs_source_configuration')); loadTestFile(require.resolve('./metrics_source_configuration')); }); diff --git a/x-pack/test/functional/apps/machine_learning/feature_controls/index.ts b/x-pack/test/functional/apps/machine_learning/feature_controls/index.ts new file mode 100644 index 000000000000..5bef6cdb7655 --- /dev/null +++ b/x-pack/test/functional/apps/machine_learning/feature_controls/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { + describe('feature controls', () => { + loadTestFile(require.resolve('./ml_security')); + loadTestFile(require.resolve('./ml_spaces')); + }); +} diff --git a/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts new file mode 100644 index 000000000000..a6a7a48e8f36 --- /dev/null +++ b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts @@ -0,0 +1,115 @@ +/* + * 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 { SecurityService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const security: SecurityService = getService('security'); + const appsMenu = getService('appsMenu'); + const PageObjects = getPageObjects(['common', 'security']); + + describe('security', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + + await security.role.create('global_all_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + base: ['all'], + spaces: ['*'], + }, + ], + }); + + // ensure we're logged out so we can login as the appropriate users + await PageObjects.security.logout(); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + await security.role.delete('global_all_role'); + + // logout, so the other tests don't accidentally run as the custom users we're testing below + await PageObjects.security.logout(); + }); + + describe('machine_learning_user', () => { + before(async () => { + await security.user.create('machine_learning_user', { + password: 'machine_learning_user-password', + roles: ['machine_learning_user'], + full_name: 'machine learning user', + }); + }); + + after(async () => { + await security.user.delete('machine_learning_user'); + }); + + it('gets forbidden after login', async () => { + await PageObjects.security.login( + 'machine_learning_user', + 'machine_learning_user-password', + { + expectForbidden: true, + } + ); + }); + }); + + describe('global all', () => { + before(async () => { + await security.user.create('global_all', { + password: 'global_all-password', + roles: ['global_all_role'], + full_name: 'global all', + }); + + await PageObjects.security.login('global_all', 'global_all-password'); + }); + + after(async () => { + await security.user.delete('global_all'); + }); + + it(`doesn't show ml navlink`, async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).not.to.contain('Machine Learning'); + }); + }); + + describe('machine_learning_user and global all', () => { + before(async () => { + await security.user.create('machine_learning_user', { + password: 'machine_learning_user-password', + roles: ['machine_learning_user', 'global_all_role'], + full_name: 'machine learning user and global all user', + }); + + await PageObjects.security.login('machine_learning_user', 'machine_learning_user-password'); + }); + + after(async () => { + await security.user.delete('machine_learning_user'); + }); + + it('shows ML navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.contain('Machine Learning'); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/machine_learning/feature_controls/ml_spaces.ts b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_spaces.ts new file mode 100644 index 000000000000..36ae88c759e1 --- /dev/null +++ b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_spaces.ts @@ -0,0 +1,93 @@ +/* + * 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 { SpacesService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const spacesService: SpacesService = getService('spaces'); + const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'error']); + const appsMenu = getService('appsMenu'); + const testSubjects = getService('testSubjects'); + + describe('spaces', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + }); + + describe('space with no features disabled', () => { + before(async () => { + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: [], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + }); + + it('shows Machine Learning navlink', async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.contain('Machine Learning'); + }); + + it(`can navigate to app`, async () => { + await PageObjects.common.navigateToApp('ml', { + basePath: '/s/custom_space', + }); + + await testSubjects.existOrFail('ml-jobs-list'); + }); + }); + + describe('space with ML disabled', () => { + before(async () => { + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: ['ml'], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + }); + + it(`doesn't show Machine Learning navlink`, async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).not.to.contain('Machine Learning'); + }); + + it(`navigating to app returns a 404`, async () => { + await PageObjects.common.navigateToUrl('ml', '', { + basePath: '/s/custom_space', + shouldLoginIfPrompted: false, + ensureCurrentUrl: false, + }); + + await PageObjects.error.expectNotFound(); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/machine_learning/index.ts b/x-pack/test/functional/apps/machine_learning/index.ts new file mode 100644 index 000000000000..14694cdf3fcb --- /dev/null +++ b/x-pack/test/functional/apps/machine_learning/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { + describe('machine learning', function() { + this.tags('ciGroup3'); + + loadTestFile(require.resolve('./feature_controls')); + }); +} diff --git a/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts b/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts new file mode 100644 index 000000000000..18708b8201bb --- /dev/null +++ b/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts @@ -0,0 +1,212 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'settings', 'security', 'maps']); + const appsMenu = getService('appsMenu'); + const testSubjects = getService('testSubjects'); + const find = getService('find'); + + const getMessageText = async () => await (await find.byCssSelector('body>pre')).getVisibleText(); + + describe('security feature controls', () => { + before(async () => { + await esArchiver.loadIfNeeded('maps/data'); + await esArchiver.load('maps/kibana'); + }); + + after(async () => { + await esArchiver.unload('maps/kibana'); + }); + + describe('global maps all privileges', () => { + before(async () => { + await security.role.create('global_maps_all_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + maps: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_maps_all_user', { + password: 'global_maps_all_user-password', + roles: ['global_maps_all_role'], + full_name: 'test user', + }); + + await PageObjects.security.logout(); + + await PageObjects.security.login('global_maps_all_user', 'global_maps_all_user-password', { + expectSpaceSelector: false, + }); + }); + + after(async () => { + await Promise.all([ + security.role.delete('global_maps_all_role'), + security.user.delete('global_maps_all_user'), + PageObjects.security.logout(), + ]); + }); + + it('shows maps navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Maps', 'Management']); + }); + + it(`allows a map to be created`, async () => { + await PageObjects.maps.openNewMap(); + await PageObjects.maps.expectExistAddLayerButton(); + await PageObjects.maps.saveMap('my test map'); + }); + + it(`allows a map to be deleted`, async () => { + await PageObjects.maps.deleteSavedMaps('my test map'); + }); + }); + + describe('global maps read-only privileges', () => { + before(async () => { + await security.role.create('global_maps_read_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + maps: ['read'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_maps_read_user', { + password: 'global_maps_read_user-password', + roles: ['global_maps_read_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'global_maps_read_user', + 'global_maps_read_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('global_maps_read_role'); + await security.user.delete('global_maps_read_user'); + }); + + it('shows Maps navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Maps', 'Management']); + }); + + it(`does not show create new button`, async () => { + await PageObjects.maps.gotoMapListingPage(); + await PageObjects.maps.expectMissingCreateNewButton(); + }); + + it(`does not allow a map to be deleted`, async () => { + await PageObjects.maps.gotoMapListingPage(); + await testSubjects.missingOrFail('checkboxSelectAll'); + }); + + describe('existing map', () => { + before(async () => { + await PageObjects.maps.loadSavedMap('document example'); + }); + + it(`can't save`, async () => { + await PageObjects.maps.expectMissingSaveButton(); + }); + + it(`can't add layer`, async () => { + await PageObjects.maps.expectMissingAddLayerButton(); + }); + }); + }); + + describe('no maps privileges', () => { + before(async () => { + await security.role.create('no_maps_privileges_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + discover: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('no_maps_privileges_user', { + password: 'no_maps_privileges_user-password', + roles: ['no_maps_privileges_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'no_maps_privileges_user', + 'no_maps_privileges_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('no_maps_privileges_role'); + await security.user.delete('no_maps_privileges_user'); + }); + + it('does not show Maps navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Discover', 'Management']); + }); + + it(`returns a 404`, async () => { + await PageObjects.common.navigateToActualUrl('maps', '', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + const messageText = await getMessageText(); + expect(messageText).to.eql( + JSON.stringify({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }) + ); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/maps/feature_controls/maps_spaces.ts b/x-pack/test/functional/apps/maps/feature_controls/maps_spaces.ts new file mode 100644 index 000000000000..d9998beeccd1 --- /dev/null +++ b/x-pack/test/functional/apps/maps/feature_controls/maps_spaces.ts @@ -0,0 +1,97 @@ +/* + * 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 { SpacesService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const spacesService: SpacesService = getService('spaces'); + const PageObjects = getPageObjects(['common', 'maps', 'security']); + const appsMenu = getService('appsMenu'); + const find = getService('find'); + + const getMessageText = async () => await (await find.byCssSelector('body>pre')).getVisibleText(); + + describe('spaces feature controls', () => { + before(async () => { + await esArchiver.loadIfNeeded('maps/data'); + }); + + describe('space with no features disabled', () => { + before(async () => { + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: [], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + }); + + it('shows Maps navlink', async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.contain('Maps'); + }); + + it(`allows a map to be created`, async () => { + await PageObjects.common.navigateToActualUrl('maps', '', { + basePath: `/s/custom_space`, + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await PageObjects.maps.saveMap('my test map'); + }); + + it(`allows a map to be deleted`, async () => { + await PageObjects.common.navigateToActualUrl('maps', '', { + basePath: `/s/custom_space`, + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await PageObjects.maps.deleteSavedMaps('my test map'); + }); + }); + + describe('space with Maps disabled', () => { + before(async () => { + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: ['maps'], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + }); + + it(`returns a 404`, async () => { + await PageObjects.common.navigateToActualUrl('maps', '', { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + const messageText = await getMessageText(); + expect(messageText).to.eql( + JSON.stringify({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }) + ); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/maps/index.js b/x-pack/test/functional/apps/maps/index.js index 49b8611eff86..9c1aeb10dcc5 100644 --- a/x-pack/test/functional/apps/maps/index.js +++ b/x-pack/test/functional/apps/maps/index.js @@ -31,6 +31,8 @@ export default function ({ loadTestFile, getService }) { this.tags('ciGroup7'); loadTestFile(require.resolve('./saved_object_management')); loadTestFile(require.resolve('./sample_data')); + loadTestFile(require.resolve('./feature_controls/maps_security')); + loadTestFile(require.resolve('./feature_controls/maps_spaces')); }); describe('', function () { diff --git a/x-pack/test/functional/apps/monitoring/feature_controls/index.ts b/x-pack/test/functional/apps/monitoring/feature_controls/index.ts new file mode 100644 index 000000000000..35611f391846 --- /dev/null +++ b/x-pack/test/functional/apps/monitoring/feature_controls/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { + describe('feature controls', () => { + loadTestFile(require.resolve('./monitoring_security')); + loadTestFile(require.resolve('./monitoring_spaces')); + }); +} diff --git a/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts new file mode 100644 index 000000000000..3e6e9cc67efa --- /dev/null +++ b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts @@ -0,0 +1,111 @@ +/* + * 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 { SecurityService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const security: SecurityService = getService('security'); + const appsMenu = getService('appsMenu'); + const PageObjects = getPageObjects(['common', 'security']); + + describe('securty', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + + await security.role.create('global_all_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + base: ['all'], + spaces: ['*'], + }, + ], + }); + + // ensure we're logged out so we can login as the appropriate users + await PageObjects.security.forceLogout(); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + await security.role.delete('global_all_role'); + + // logout, so the other tests don't accidentally run as the custom users we're testing below + await PageObjects.security.forceLogout(); + }); + + describe('monitoring_user', () => { + before(async () => { + await security.user.create('monitoring_user', { + password: 'monitoring_user-password', + roles: ['monitoring_user'], + full_name: 'monitoring all', + }); + }); + + after(async () => { + await security.user.delete('monitoring_user'); + }); + + it('gets forbidden after login', async () => { + await PageObjects.security.login('monitoring_user', 'monitoring_user-password', { + expectForbidden: true, + }); + }); + }); + + describe('global all', () => { + before(async () => { + await security.user.create('global_all', { + password: 'global_all-password', + roles: ['global_all_role'], + full_name: 'global all', + }); + + await PageObjects.security.login('global_all', 'global_all-password'); + }); + + after(async () => { + await security.user.delete('global_all'); + }); + + it(`doesn't show monitoring navlink`, async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).not.to.contain('Stack Monitoring'); + }); + }); + + describe('monitoring_user and global all', () => { + before(async () => { + await security.user.create('monitoring_user', { + password: 'monitoring_user-password', + roles: ['monitoring_user', 'global_all_role'], + full_name: 'monitoring user', + }); + + await PageObjects.security.login('monitoring_user', 'monitoring_user-password'); + }); + + after(async () => { + await security.user.delete('monitoring_user'); + }); + + it('shows monitoring navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.contain('Stack Monitoring'); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts new file mode 100644 index 000000000000..58282269a542 --- /dev/null +++ b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { SpacesService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const spacesService: SpacesService = getService('spaces'); + const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'error']); + const appsMenu = getService('appsMenu'); + const find = getService('find'); + + describe('spaces', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + }); + + describe('space with no features disabled', () => { + before(async () => { + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: [], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + }); + + it('shows Stack Monitoring navlink', async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.contain('Stack Monitoring'); + }); + + it(`can navigate to app`, async () => { + await PageObjects.common.navigateToApp('monitoring', { + basePath: '/s/custom_space', + }); + + const exists = await find.existsByCssSelector('monitoring-main'); + expect(exists).to.be(true); + }); + }); + + describe('space with monitoring disabled', () => { + before(async () => { + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: ['monitoring'], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + }); + + it(`doesn't show Stack Monitoring navlink`, async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).not.to.contain('Stack Monitoring'); + }); + + it(`navigating to app returns a 404`, async () => { + await PageObjects.common.navigateToUrl('monitoring', '', { + basePath: '/s/custom_space', + shouldLoginIfPrompted: false, + ensureCurrentUrl: false, + }); + + await PageObjects.error.expectNotFound(); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/monitoring/index.js b/x-pack/test/functional/apps/monitoring/index.js index a1551f2829a9..a22ad50d1338 100644 --- a/x-pack/test/functional/apps/monitoring/index.js +++ b/x-pack/test/functional/apps/monitoring/index.js @@ -8,6 +8,8 @@ export default function ({ loadTestFile }) { describe('Monitoring app', function () { this.tags('ciGroup1'); + loadTestFile(require.resolve('./feature_controls')); + loadTestFile(require.resolve('./cluster/list')); loadTestFile(require.resolve('./cluster/overview')); loadTestFile(require.resolve('./cluster/alerts')); diff --git a/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts b/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts new file mode 100644 index 000000000000..956bb3f60871 --- /dev/null +++ b/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts @@ -0,0 +1,232 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['common', 'settings', 'security']); + + describe('feature controls saved objects management', () => { + before(async () => { + await esArchiver.load('saved_objects_management/feature_controls/security'); + }); + + after(async () => { + await esArchiver.unload('saved_objects_management/feature_controls/security'); + }); + + describe('global all privileges', () => { + before(async () => { + await security.role.create('global_all_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + base: ['all'], + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_all_user', { + password: 'global_all_user-password', + roles: ['global_all_role'], + full_name: 'test user', + }); + + await PageObjects.security.logout(); + + await PageObjects.security.login('global_all_user', 'global_all_user-password', { + expectSpaceSelector: false, + }); + }); + + after(async () => { + await Promise.all([ + security.role.delete('global_all_role'), + security.user.delete('global_all_user'), + PageObjects.security.logout(), + ]); + }); + + describe('listing', () => { + before(async () => { + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaSavedObjects(); + }); + + it('shows all saved objects', async () => { + const objects = await PageObjects.settings.getSavedObjectsInTable(); + expect(objects).to.eql(['A Dashboard', 'logstash-*', 'A Pie']); + }); + + it('can view all saved objects in applications', async () => { + const bools = await PageObjects.settings.getSavedObjectsTableSummary(); + expect(bools).to.eql([ + { + title: 'A Dashboard', + canViewInApp: true, + }, + { + title: 'logstash-*', + canViewInApp: true, + }, + { + title: 'A Pie', + canViewInApp: true, + }, + ]); + }); + + it('can delete all saved objects', async () => { + await PageObjects.settings.clickSavedObjectsTableSelectAll(); + const actual = await PageObjects.settings.canSavedObjectsBeDeleted(); + expect(actual).to.be(true); + }); + }); + + describe('edit visualization', () => { + before(async () => { + await PageObjects.common.navigateToActualUrl( + 'kibana', + '/management/kibana/objects/savedVisualizations/75c3e060-1e7c-11e9-8488-65449e65d0ed', + { + loginIfPrompted: false, + } + ); + }); + + it('shows delete button', async () => { + await testSubjects.existOrFail('savedObjectEditDelete'); + }); + + it('shows save button', async () => { + await testSubjects.existOrFail('savedObjectEditSave'); + }); + + it('has inputs without readonly attributes', async () => { + const form = await testSubjects.find('savedObjectEditForm'); + const inputs = await form.findAllByCssSelector('input'); + expect(inputs.length).to.be.greaterThan(0); + for (const input of inputs) { + const isEnabled = await input.isEnabled(); + expect(isEnabled).to.be(true); + } + }); + }); + }); + + describe('global visualize read privileges', () => { + before(async () => { + await security.role.create('global_visualize_all_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + visualize: ['read'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_visualize_all_user', { + password: 'global_visualize_all_user-password', + roles: ['global_visualize_all_role'], + full_name: 'test user', + }); + + await PageObjects.security.logout(); + + await PageObjects.security.login( + 'global_visualize_all_user', + 'global_visualize_all_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await Promise.all([ + security.role.delete('global_visualize_all_role'), + security.user.delete('global_visualize_all_user'), + PageObjects.security.logout(), + ]); + }); + + describe('listing', () => { + before(async () => { + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaSavedObjects(); + }); + + it('shows a visualization and an index pattern', async () => { + const objects = await PageObjects.settings.getSavedObjectsInTable(); + expect(objects).to.eql(['logstash-*', 'A Pie']); + }); + + it('can view only the visualization in application', async () => { + const bools = await PageObjects.settings.getSavedObjectsTableSummary(); + expect(bools).to.eql([ + { + title: 'logstash-*', + canViewInApp: false, + }, + { + title: 'A Pie', + canViewInApp: true, + }, + ]); + }); + + it(`can't delete all saved objects`, async () => { + await PageObjects.settings.clickSavedObjectsTableSelectAll(); + const actual = await PageObjects.settings.canSavedObjectsBeDeleted(); + expect(actual).to.be(false); + }); + }); + + describe('edit visualization', () => { + before(async () => { + await PageObjects.common.navigateToActualUrl( + 'kibana', + '/management/kibana/objects/savedVisualizations/75c3e060-1e7c-11e9-8488-65449e65d0ed', + { + loginIfPrompted: false, + } + ); + await testSubjects.existOrFail('savedObjectsEdit'); + }); + + it('shows delete button', async () => { + await testSubjects.missingOrFail('savedObjectEditDelete'); + }); + + it('shows save button', async () => { + await testSubjects.missingOrFail('savedObjectEditSave'); + }); + + it('has inputs without readonly attributes', async () => { + const form = await testSubjects.find('savedObjectEditForm'); + const inputs = await form.findAllByCssSelector('input'); + expect(inputs.length).to.be.greaterThan(0); + for (const input of inputs) { + const isEnabled = await input.isEnabled(); + expect(isEnabled).to.be(false); + } + }); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/saved_objects_management/index.ts b/x-pack/test/functional/apps/saved_objects_management/index.ts new file mode 100644 index 000000000000..b0a0adeb63f1 --- /dev/null +++ b/x-pack/test/functional/apps/saved_objects_management/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function advancedSettingsApp({ + loadTestFile, +}: KibanaFunctionalTestDefaultProviders) { + describe('Saved objects management', function savedObjectsManagementAppTestSuite() { + this.tags('ciGroup2'); + loadTestFile(require.resolve('./feature_controls/saved_objects_management_security')); + }); +} diff --git a/x-pack/test/functional/apps/security/rbac_phase1.js b/x-pack/test/functional/apps/security/rbac_phase1.js index e8c70b0d9644..fbe2f8b66c59 100644 --- a/x-pack/test/functional/apps/security/rbac_phase1.js +++ b/x-pack/test/functional/apps/security/rbac_phase1.js @@ -20,7 +20,7 @@ export default function ({ getService, getPageObjects }) { log.debug('users'); await esArchiver.loadIfNeeded('logstash_functional'); log.debug('load kibana index with default index pattern'); - await esArchiver.load('discover'); + await esArchiver.load('security/discover'); await kibanaServer.uiSettings.replace({ 'defaultIndex': 'logstash-*' }); await PageObjects.settings.navigateTo(); await PageObjects.security.clickElasticsearchRoles(); @@ -103,26 +103,6 @@ export default function ({ getService, getPageObjects }) { await PageObjects.security.logout(); }); - it('rbac read only role can not save a visualization', async function () { - const fromTime = '2015-09-19 06:31:44.000'; - const toTime = '2015-09-23 18:31:44.000'; - const vizName1 = 'Viz VerticalBarChart'; - - log.debug('log in as kibanareadonly with rbac_read role'); - await PageObjects.security.login('kibanareadonly', 'changeme'); - log.debug('navigateToApp visualize'); - await PageObjects.visualize.navigateToNewVisualization(); - log.debug('clickVerticalBarChart'); - await PageObjects.visualize.clickVerticalBarChart(); - await PageObjects.visualize.clickNewSearch(); - log.debug('Set absolute time range from \"' + fromTime + '\" to \"' + toTime + '\"'); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); - await PageObjects.visualize.waitForVisualization(); - await PageObjects.visualize.saveVisualizationExpectFail(vizName1); - await PageObjects.security.logout(); - - }); - after(async function () { await PageObjects.security.logout(); }); diff --git a/x-pack/test/functional/apps/security/secure_roles_perm.js b/x-pack/test/functional/apps/security/secure_roles_perm.js index fb5effa62775..4f725d04ce03 100644 --- a/x-pack/test/functional/apps/security/secure_roles_perm.js +++ b/x-pack/test/functional/apps/security/secure_roles_perm.js @@ -24,7 +24,7 @@ export default function ({ getService, getPageObjects }) { log.debug('users'); await esArchiver.loadIfNeeded('logstash_functional'); log.debug('load kibana index with default index pattern'); - await esArchiver.load('discover'); + await esArchiver.load('security/discover'); await kibanaServer.uiSettings.replace({ 'defaultIndex': 'logstash-*' }); await PageObjects.settings.navigateTo(); }); @@ -64,15 +64,6 @@ export default function ({ getService, getPageObjects }) { await PageObjects.security.login('Rashmi', 'changeme'); }); - //Verify the Access Denied message is displayed - it('Kibana User navigating to Monitoring gets Access Denied', async function () { - const expectedMessage = 'Access Denied'; - await PageObjects.monitoring.navigateTo(); - const actualMessage = await PageObjects.monitoring.getAccessDeniedMessage(); - expect(actualMessage).to.be(expectedMessage); - }); - - it('Kibana User navigating to Management gets permission denied', async function () { await PageObjects.settings.navigateTo(); await PageObjects.security.clickElasticsearchUsers(); diff --git a/x-pack/test/functional/apps/security/user_email.js b/x-pack/test/functional/apps/security/user_email.js index d4828b61053d..b79e32fc6a7d 100644 --- a/x-pack/test/functional/apps/security/user_email.js +++ b/x-pack/test/functional/apps/security/user_email.js @@ -14,7 +14,7 @@ export default function ({ getService, getPageObjects }) { describe('useremail', function () { before(async () => { - await esArchiver.load('discover'); + await esArchiver.load('security/discover'); await PageObjects.settings.navigateTo(); await PageObjects.security.clickElasticsearchUsers(); }); diff --git a/x-pack/test/functional/apps/spaces/index.ts b/x-pack/test/functional/apps/spaces/index.ts index 3b8a1ad3405b..b8b4e71b141f 100644 --- a/x-pack/test/functional/apps/spaces/index.ts +++ b/x-pack/test/functional/apps/spaces/index.ts @@ -3,10 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { TestInvoker } from './lib/types'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; // eslint-disable-next-line import/no-default-export -export default function spacesApp({ loadTestFile }: TestInvoker) { +export default function spacesApp({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { describe('Spaces app', function spacesAppTestSuite() { this.tags('ciGroup4'); diff --git a/x-pack/test/functional/apps/spaces/lib/types.ts b/x-pack/test/functional/apps/spaces/lib/types.ts deleted file mode 100644 index 2ed91406e5f4..000000000000 --- a/x-pack/test/functional/apps/spaces/lib/types.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export type DescribeFn = (text: string, fn: () => void) => void; - -export interface TestDefinitionAuthentication { - username?: string; - password?: string; -} - -export type LoadTestFileFn = (path: string) => string; - -export type GetServiceFn = (service: string) => any; - -export type ReadConfigFileFn = (path: string) => any; - -export type GetPageObjectsFn = (pageObjects: string[]) => any; - -export interface TestInvoker { - getService: GetServiceFn; - getPageObjects: GetPageObjectsFn; - loadTestFile: LoadTestFileFn; - readConfigFile: ReadConfigFileFn; -} diff --git a/x-pack/test/functional/apps/spaces/spaces_selection.ts b/x-pack/test/functional/apps/spaces/spaces_selection.ts index 92ab67731170..578ca05dbee9 100644 --- a/x-pack/test/functional/apps/spaces/spaces_selection.ts +++ b/x-pack/test/functional/apps/spaces/spaces_selection.ts @@ -3,11 +3,13 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { TestInvoker } from './lib/types'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; // eslint-disable-next-line import/no-default-export -export default function spaceSelectorFunctonalTests({ getService, getPageObjects }: TestInvoker) { - const config = getService('config'); +export default function spaceSelectorFunctonalTests({ + getService, + getPageObjects, +}: KibanaFunctionalTestDefaultProviders) { const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects([ 'common', @@ -20,8 +22,8 @@ export default function spaceSelectorFunctonalTests({ getService, getPageObjects describe('Spaces', () => { describe('Space Selector', () => { - before(async () => await esArchiver.load('spaces')); - after(async () => await esArchiver.unload('spaces')); + before(async () => await esArchiver.load('spaces/selector')); + after(async () => await esArchiver.unload('spaces/selector')); afterEach(async () => { await PageObjects.security.logout(); @@ -50,8 +52,6 @@ export default function spaceSelectorFunctonalTests({ getService, getPageObjects describe('Spaces Data', () => { const spaceId = 'another-space'; - const dashboardPath = config.get(['apps', 'dashboard']).pathname; - const homePath = config.get(['apps', 'home']).pathname; const sampleDataHash = '/home/tutorial_directory/sampleData'; const expectDashboardRenders = async (dashName: string) => { @@ -62,22 +62,18 @@ export default function spaceSelectorFunctonalTests({ getService, getPageObjects }; before(async () => { - await esArchiver.load('spaces'); + await esArchiver.load('spaces/selector'); await PageObjects.security.login(null, null, { expectSpaceSelector: true, }); await PageObjects.spaceSelector.clickSpaceCard('default'); await PageObjects.common.navigateToApp('home', { - appConfig: { - hash: sampleDataHash, - }, + hash: sampleDataHash, }); await PageObjects.home.addSampleDataSet('logs'); await PageObjects.common.navigateToApp('home', { - appConfig: { - hash: sampleDataHash, - pathname: `/s/${spaceId}${homePath}`, - }, + hash: sampleDataHash, + basePath: `/s/${spaceId}`, }); await PageObjects.home.addSampleDataSet('logs'); }); @@ -88,13 +84,11 @@ export default function spaceSelectorFunctonalTests({ getService, getPageObjects // the created saved objects in the second space will be broken but removed // when we call esArchiver.unload('spaces'). await PageObjects.common.navigateToApp('home', { - appConfig: { - hash: sampleDataHash, - }, + hash: sampleDataHash, }); await PageObjects.home.removeSampleDataSet('logs'); await PageObjects.security.logout(); - await esArchiver.unload('spaces'); + await esArchiver.unload('spaces/selector'); }); describe('displays separate data for each space', async () => { @@ -105,9 +99,7 @@ export default function spaceSelectorFunctonalTests({ getService, getPageObjects it('in a custom space', async () => { await PageObjects.common.navigateToApp('dashboard', { - appConfig: { - pathname: `/s/${spaceId}${dashboardPath}`, - }, + basePath: `/s/${spaceId}`, }); await expectDashboardRenders('[Logs] Web Traffic'); }); diff --git a/x-pack/test/functional/apps/status_page/status_page.ts b/x-pack/test/functional/apps/status_page/status_page.ts index e3f345157477..88203b350c26 100644 --- a/x-pack/test/functional/apps/status_page/status_page.ts +++ b/x-pack/test/functional/apps/status_page/status_page.ts @@ -15,6 +15,7 @@ export default function statusPageFunctonalTests({ getService, getPageObjects }: after(async () => await esArchiver.unload('empty_kibana')); it('allows user to navigate without authentication', async () => { + await PageObjects.security.logout(); await PageObjects.statusPage.navigateToPage(); await PageObjects.statusPage.expectStatusPage(); }); diff --git a/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts b/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts new file mode 100644 index 000000000000..77cfa8e48a41 --- /dev/null +++ b/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts @@ -0,0 +1,183 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'timelion', 'header', 'security', 'spaceSelector']); + const find = getService('find'); + const appsMenu = getService('appsMenu'); + + describe('feature controls security', () => { + before(async () => { + await esArchiver.loadIfNeeded('timelion/feature_controls'); + await esArchiver.loadIfNeeded('logstash_functional'); + }); + + describe('global timelion all privileges', () => { + before(async () => { + await security.role.create('global_timelion_all_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + timelion: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_timelion_all_user', { + password: 'global_timelion_all_user-password', + roles: ['global_timelion_all_role'], + full_name: 'test user', + }); + + await PageObjects.security.logout(); + + await PageObjects.security.login( + 'global_timelion_all_user', + 'global_timelion_all_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await PageObjects.security.logout(); + await security.role.delete('global_timelion_all_role'); + await security.user.delete('global_timelion_all_user'); + }); + + it('shows timelion navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Timelion', 'Management']); + }); + + it(`allows a timelion sheet to be created`, async () => { + await PageObjects.common.navigateToApp('timelion'); + await PageObjects.timelion.saveTimelionSheet(); + }); + }); + + describe('global timelion read-only privileges', () => { + before(async () => { + await security.role.create('global_timelion_read_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + timelion: ['read'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_timelion_read_user', { + password: 'global_timelion_read_user-password', + roles: ['global_timelion_read_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'global_timelion_read_user', + 'global_timelion_read_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await PageObjects.security.logout(); + await security.role.delete('global_timelion_read_role'); + await security.user.delete('global_timelion_read_user'); + }); + + it('shows timelion navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Timelion', 'Management']); + }); + + it(`does not allow a timelion sheet to be created`, async () => { + await PageObjects.common.navigateToApp('timelion'); + await PageObjects.timelion.expectMissingWriteControls(); + }); + }); + + describe('no timelion privileges', () => { + before(async () => { + await security.role.create('no_timelion_privileges_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + discover: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('no_timelion_privileges_user', { + password: 'no_timelion_privileges_user-password', + roles: ['no_timelion_privileges_role'], + full_name: 'test user', + }); + + await PageObjects.security.logout(); + + await PageObjects.security.login( + 'no_timelion_privileges_user', + 'no_timelion_privileges_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await PageObjects.security.logout(); + await security.role.delete('no_timelion_privileges_role'); + await security.user.delete('no_timelion_privileges_user'); + }); + + const getMessageText = async () => + await (await find.byCssSelector('body>pre')).getVisibleText(); + + it(`returns a 404`, async () => { + await PageObjects.common.navigateToActualUrl('timelion', '', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + const messageText = await getMessageText(); + expect(messageText).to.eql( + JSON.stringify({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }) + ); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/timelion/feature_controls/timelion_spaces.ts b/x-pack/test/functional/apps/timelion/feature_controls/timelion_spaces.ts new file mode 100644 index 000000000000..68a3000a0c23 --- /dev/null +++ b/x-pack/test/functional/apps/timelion/feature_controls/timelion_spaces.ts @@ -0,0 +1,137 @@ +/* + * 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 { SpacesService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const spacesService: SpacesService = getService('spaces'); + const PageObjects = getPageObjects(['common', 'timelion', 'security', 'spaceSelector']); + const find = getService('find'); + const appsMenu = getService('appsMenu'); + + const getMessageText = async () => await (await find.byCssSelector('body>pre')).getVisibleText(); + + describe('timelion', () => { + before(async () => { + await esArchiver.loadIfNeeded('logstash_functional'); + }); + + describe('space with no features disabled', () => { + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('timelion/feature_controls'); + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: [], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('timelion/feature_controls'); + }); + + it('shows timelion navlink', async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.contain('Timelion'); + }); + + it(`allows a timelion sheet to be created`, async () => { + await PageObjects.common.navigateToApp('timelion'); + await PageObjects.timelion.saveTimelionSheet(); + }); + }); + + describe('space with Timelion disabled', () => { + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('timelion/feature_controls'); + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: ['timelion'], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('timelion/feature_controls'); + }); + + it(`doesn't show timelion navlink`, async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).not.to.contain('Timelion'); + }); + + it(`create new timelion returns a 404`, async () => { + await PageObjects.common.navigateToActualUrl('timelion', 'i-exist', { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + + const messageText = await getMessageText(); + expect(messageText).to.eql( + JSON.stringify({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }) + ); + }); + + it(`edit timelion sheet which doesn't exist returns a 404`, async () => { + await PageObjects.common.navigateToActualUrl('timelion', 'i-dont-exist', { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + + const messageText = await getMessageText(); + expect(messageText).to.eql( + JSON.stringify({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }) + ); + }); + + it(`edit timelion sheet which exists returns a 404`, async () => { + await PageObjects.common.navigateToActualUrl('timelion', 'i-exist', { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + + const messageText = await getMessageText(); + expect(messageText).to.eql( + JSON.stringify({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }) + ); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/timelion/index.ts b/x-pack/test/functional/apps/timelion/index.ts new file mode 100644 index 000000000000..2dfed6f89d9e --- /dev/null +++ b/x-pack/test/functional/apps/timelion/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function timelion({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { + describe('Timelion', function visualizeTestSuite() { + this.tags('ciGroup4'); + + loadTestFile(require.resolve('./feature_controls/timelion_security')); + loadTestFile(require.resolve('./feature_controls/timelion_spaces')); + }); +} diff --git a/x-pack/test/functional/apps/uptime/feature_controls/index.ts b/x-pack/test/functional/apps/uptime/feature_controls/index.ts new file mode 100644 index 000000000000..d4a948ea97e5 --- /dev/null +++ b/x-pack/test/functional/apps/uptime/feature_controls/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { + describe('feature controls', () => { + loadTestFile(require.resolve('./uptime_security')); + loadTestFile(require.resolve('./uptime_spaces')); + }); +} diff --git a/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts b/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts new file mode 100644 index 000000000000..ae563b430636 --- /dev/null +++ b/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts @@ -0,0 +1,181 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { SecurityService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const security: SecurityService = getService('security'); + const PageObjects = getPageObjects(['common', 'error', 'timePicker', 'security']); + const testSubjects = getService('testSubjects'); + const appsMenu = getService('appsMenu'); + + describe('security', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + // ensure we're logged out so we can login as the appropriate users + await PageObjects.security.forceLogout(); + }); + + after(async () => { + // logout, so the other tests don't accidentally run as the custom users we're testing below + await PageObjects.security.forceLogout(); + }); + + describe('global uptime all privileges', () => { + before(async () => { + await security.role.create('global_uptime_all_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + uptime: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_uptime_all_user', { + password: 'global_uptime_all_user-password', + roles: ['global_uptime_all_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'global_uptime_all_user', + 'global_uptime_all_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('global_uptime_all_role'); + await security.user.delete('global_uptime_all_user'); + }); + + it('shows uptime navlink', async () => { + const navLinks = await appsMenu.readLinks(); + expect(navLinks.map((link: Record) => link.text)).to.eql([ + 'Uptime', + 'Management', + ]); + }); + + it('can navigate to Uptime app', async () => { + await PageObjects.common.navigateToApp('uptime'); + await testSubjects.existOrFail('uptimeApp', 10000); + }); + }); + + describe('global uptime read-only privileges', () => { + before(async () => { + await security.role.create('global_uptime_read_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + uptime: ['read'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_uptime_read_user', { + password: 'global_uptime_read_user-password', + roles: ['global_uptime_read_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'global_uptime_read_user', + 'global_uptime_read_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('global_uptime_read_role'); + await security.user.delete('global_uptime_read_user'); + }); + + it('shows uptime navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Uptime', 'Management']); + }); + + it('can navigate to Uptime app', async () => { + await PageObjects.common.navigateToApp('uptime'); + await testSubjects.existOrFail('uptimeApp', 10000); + }); + }); + + describe('no uptime privileges', () => { + before(async () => { + await security.role.create('no_uptime_privileges_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + dashboard: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('no_uptime_privileges_user', { + password: 'no_uptime_privileges_user-password', + roles: ['no_uptime_privileges_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'no_uptime_privileges_user', + 'no_uptime_privileges_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('no_uptime_privileges_role'); + await security.user.delete('no_uptime_privileges_user'); + }); + + it(`doesn't show uptime navlink`, async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).not.to.contain('Uptime'); + }); + + it(`renders not found page`, async () => { + await PageObjects.common.navigateToUrl('uptime', '', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await PageObjects.error.expectNotFound(); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/uptime/feature_controls/uptime_spaces.ts b/x-pack/test/functional/apps/uptime/feature_controls/uptime_spaces.ts new file mode 100644 index 000000000000..cf23bff4be77 --- /dev/null +++ b/x-pack/test/functional/apps/uptime/feature_controls/uptime_spaces.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { SpacesService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const spacesService: SpacesService = getService('spaces'); + const PageObjects = getPageObjects(['common', 'error', 'timePicker', 'security']); + const testSubjects = getService('testSubjects'); + const appsMenu = getService('appsMenu'); + + describe('spaces', () => { + describe('space with no features disabled', () => { + before(async () => { + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: [], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + }); + + it('shows uptime navlink', async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.contain('Uptime'); + }); + + it('can navigate to Uptime app', async () => { + await PageObjects.common.navigateToApp('uptime'); + await testSubjects.existOrFail('uptimeApp', 10000); + }); + }); + + describe('space with Uptime disabled', () => { + before(async () => { + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: ['uptime'], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + }); + + it(`doesn't show uptime navlink`, async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).not.to.contain('Uptime'); + }); + + it(`renders not found page`, async () => { + await PageObjects.common.navigateToUrl('uptime', '', { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await PageObjects.error.expectNotFound(); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/uptime/index.ts b/x-pack/test/functional/apps/uptime/index.ts index fdc97a1128aa..7a53ce7d32aa 100644 --- a/x-pack/test/functional/apps/uptime/index.ts +++ b/x-pack/test/functional/apps/uptime/index.ts @@ -21,6 +21,7 @@ export default ({ loadTestFile, getService }: KibanaFunctionalTestDefaultProvide after(async () => await esArchiver.unload(ARCHIVE)); this.tags('ciGroup6'); + loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./overview')); loadTestFile(require.resolve('./monitor')); }); diff --git a/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts b/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts new file mode 100644 index 000000000000..fdf22ff7bede --- /dev/null +++ b/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts @@ -0,0 +1,247 @@ +/* + * 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 { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const PageObjects = getPageObjects([ + 'common', + 'visualize', + 'header', + 'security', + 'share', + 'spaceSelector', + 'timePicker', + ]); + const testSubjects = getService('testSubjects'); + const appsMenu = getService('appsMenu'); + + describe('feature controls security', () => { + before(async () => { + await esArchiver.load('visualize/default'); + await esArchiver.loadIfNeeded('logstash_functional'); + }); + + after(async () => { + await esArchiver.unload('visualize/default'); + }); + + describe('global visualize all privileges', () => { + before(async () => { + await security.role.create('global_visualize_all_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + visualize: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_visualize_all_user', { + password: 'global_visualize_all_user-password', + roles: ['global_visualize_all_role'], + full_name: 'test user', + }); + + await PageObjects.security.logout(); + + await PageObjects.security.login( + 'global_visualize_all_user', + 'global_visualize_all_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await PageObjects.security.logout(); + await security.role.delete('global_visualize_all_role'); + await security.user.delete('global_visualize_all_user'); + }); + + it('shows visualize navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Visualize', 'Management']); + }); + + it(`landing page shows "Create new Visualization" button`, async () => { + await PageObjects.visualize.gotoVisualizationLandingPage(); + await testSubjects.existOrFail('visualizeLandingPage', 10000); + await testSubjects.existOrFail('newItemButton'); + }); + + it(`can view existing Visualization`, async () => { + await PageObjects.common.navigateToActualUrl('kibana', '/visualize/edit/i-exist', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('visualizationLoader', 10000); + }); + + it('can save existing Visualization', async () => { + await PageObjects.common.navigateToActualUrl('kibana', '/visualize/edit/i-exist', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('visualizeSaveButton', 10000); + }); + + it('Embed code shows create short-url button', async () => { + await PageObjects.share.openShareMenuItem('Embedcode'); + await PageObjects.share.createShortUrlExistOrFail(); + }); + + it('Permalinks shows create short-url button', async () => { + await PageObjects.share.openShareMenuItem('Permalinks'); + await PageObjects.share.createShortUrlExistOrFail(); + }); + }); + + describe('global visualize read-only privileges', () => { + before(async () => { + await security.role.create('global_visualize_read_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + visualize: ['read'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_visualize_read_user', { + password: 'global_visualize_read_user-password', + roles: ['global_visualize_read_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'global_visualize_read_user', + 'global_visualize_read_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await PageObjects.security.logout(); + await security.role.delete('global_visualize_read_role'); + await security.user.delete('global_visualize_read_user'); + }); + + it('shows visualize navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Visualize', 'Management']); + }); + + it(`landing page shows "Create new Visualization" button`, async () => { + await PageObjects.visualize.gotoVisualizationLandingPage(); + await testSubjects.existOrFail('visualizeLandingPage', 10000); + await testSubjects.existOrFail('newItemButton'); + }); + + it(`can view existing Visualization`, async () => { + await PageObjects.common.navigateToActualUrl('visualize', '/visualize/edit/i-exist', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('visualizationLoader', 10000); + }); + + it(`can't save existing Visualization`, async () => { + await PageObjects.common.navigateToActualUrl('visualize', '/visualize/edit/i-exist', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('shareTopNavButton', 10000); + await testSubjects.missingOrFail('visualizeSaveButton', 10000); + }); + + it(`Embed Code doesn't show create short-url button`, async () => { + await PageObjects.share.openShareMenuItem('Embedcode'); + await PageObjects.share.createShortUrlMissingOrFail(); + }); + + it(`Permalinks doesn't show create short-url button`, async () => { + await PageObjects.share.openShareMenuItem('Permalinks'); + await PageObjects.share.createShortUrlMissingOrFail(); + }); + }); + + describe('no visualize privileges', () => { + before(async () => { + await security.role.create('no_visualize_privileges_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + discover: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('no_visualize_privileges_user', { + password: 'no_visualize_privileges_user-password', + roles: ['no_visualize_privileges_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'no_visualize_privileges_user', + 'no_visualize_privileges_user-password', + { + expectSpaceSelector: false, + shouldLoginIfPrompted: false, + } + ); + }); + + after(async () => { + await PageObjects.security.logout(); + await security.role.delete('no_visualize_privileges_role'); + await security.user.delete('no_visualize_privileges_user'); + }); + + it(`landing page redirects to home page`, async () => { + await PageObjects.common.navigateToActualUrl('visualize', '', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('homeApp', 10000); + }); + + it(`edit page redirects to home page`, async () => { + await PageObjects.common.navigateToActualUrl('visualize', '/visualize/edit/i-exist', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('homeApp', 10000); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/visualize/feature_controls/visualize_spaces.ts b/x-pack/test/functional/apps/visualize/feature_controls/visualize_spaces.ts new file mode 100644 index 000000000000..e69878f3200d --- /dev/null +++ b/x-pack/test/functional/apps/visualize/feature_controls/visualize_spaces.ts @@ -0,0 +1,129 @@ +/* + * 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'; +// eslint-disable-next-line max-len +import { VisualizeConstants } from '../../../../../../src/legacy/core_plugins/kibana/public/visualize/visualize_constants'; +import { SpacesService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const spacesService: SpacesService = getService('spaces'); + const PageObjects = getPageObjects(['common', 'visualize', 'security', 'spaceSelector']); + const testSubjects = getService('testSubjects'); + const appsMenu = getService('appsMenu'); + + describe('visualize', () => { + before(async () => { + await esArchiver.loadIfNeeded('logstash_functional'); + }); + + describe('space with no features disabled', () => { + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('visualize/default'); + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: [], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('visualize/default'); + }); + + it('shows visualize navlink', async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.contain('Visualize'); + }); + + it(`can view existing Visualization`, async () => { + await PageObjects.common.navigateToActualUrl( + 'kibana', + `${VisualizeConstants.EDIT_PATH}/i-exist`, + { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + } + ); + await testSubjects.existOrFail('visualizationLoader', 10000); + }); + }); + + describe('space with Visualize disabled', () => { + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('visualize/default'); + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: ['visualize'], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('visualize/default'); + }); + + it(`doesn't show visualize navlink`, async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).not.to.contain('Visualize'); + }); + + it(`create new visualization redirects to the home page`, async () => { + await PageObjects.common.navigateToActualUrl('kibana', VisualizeConstants.CREATE_PATH, { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('homeApp', 10000); + }); + + it(`edit visualization for object which doesn't exist redirects to the home page`, async () => { + await PageObjects.common.navigateToActualUrl( + 'kibana', + `${VisualizeConstants.EDIT_PATH}/i-dont-exist`, + { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + } + ); + await testSubjects.existOrFail('homeApp', 10000); + }); + + it(`edit visualization for object which exists redirects to the home page`, async () => { + await PageObjects.common.navigateToActualUrl( + 'kibana', + `${VisualizeConstants.EDIT_PATH}/i-exist`, + { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + } + ); + await testSubjects.existOrFail('homeApp', 10000); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/visualize/index.ts b/x-pack/test/functional/apps/visualize/index.ts new file mode 100644 index 000000000000..0469a4f0d2cd --- /dev/null +++ b/x-pack/test/functional/apps/visualize/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function visualize({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { + describe('Visualize', function visualizeTestSuite() { + this.tags('ciGroup4'); + + loadTestFile(require.resolve('./feature_controls/visualize_security')); + loadTestFile(require.resolve('./feature_controls/visualize_spaces')); + }); +} diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index 0596de6e622d..5c542ea3e17a 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -9,6 +9,7 @@ import { resolve } from 'path'; import { + CanvasPageProvider, SecurityPageProvider, MonitoringPageProvider, LogstashPageProvider, @@ -59,6 +60,11 @@ import { InfraSourceConfigurationFlyoutProvider, } from './services'; +import { + SecurityServiceProvider, + SpacesServiceProvider, +} from '../common/services'; + // the default export of config files must be a config provider // that returns an object with the projects config values export default async function ({ readConfigFile }) { @@ -75,21 +81,31 @@ export default async function ({ readConfigFile }) { return { // list paths to the files that contain your plugins tests testFiles: [ + resolve(__dirname, './apps/advanced_settings'), resolve(__dirname, './apps/canvas'), resolve(__dirname, './apps/graph'), resolve(__dirname, './apps/monitoring'), resolve(__dirname, './apps/watcher'), + resolve(__dirname, './apps/dashboard'), resolve(__dirname, './apps/dashboard_mode'), + resolve(__dirname, './apps/discover'), resolve(__dirname, './apps/security'), resolve(__dirname, './apps/spaces'), resolve(__dirname, './apps/logstash'), resolve(__dirname, './apps/grok_debugger'), resolve(__dirname, './apps/infra'), + resolve(__dirname, './apps/machine_learning'), resolve(__dirname, './apps/rollup_job'), resolve(__dirname, './apps/maps'), resolve(__dirname, './apps/status_page'), + resolve(__dirname, './apps/timelion'), resolve(__dirname, './apps/upgrade_assistant'), + resolve(__dirname, './apps/visualize'), resolve(__dirname, './apps/uptime'), + resolve(__dirname, './apps/saved_objects_management'), + resolve(__dirname, './apps/dev_tools'), + resolve(__dirname, './apps/apm'), + resolve(__dirname, './apps/index_patterns') ], // define the name and providers for services that should be @@ -124,6 +140,8 @@ export default async function ({ readConfigFile }) { random: RandomProvider, aceEditor: AceEditorProvider, grokDebugger: GrokDebuggerProvider, + security: SecurityServiceProvider, + spaces: SpacesServiceProvider, userMenu: UserMenuProvider, uptime: UptimeProvider, rollup: RollupPageProvider, @@ -134,6 +152,7 @@ export default async function ({ readConfigFile }) { // names to Providers. Merge in Kibana's or pick specific ones pageObjects: { ...kibanaFunctionalConfig.get('pageObjects'), + canvas: CanvasPageProvider, security: SecurityPageProvider, accountSetting: AccountSettingProvider, monitoring: MonitoringPageProvider, @@ -202,6 +221,10 @@ export default async function ({ readConfigFile }) { pathname: '/app/kibana', hash: '/dev_tools/grokdebugger', }, + searchProfiler: { + pathname: '/app/kibana', + hash: '/dev_tools/searchprofiler', + }, spaceSelector: { pathname: '/', }, @@ -219,10 +242,19 @@ export default async function ({ readConfigFile }) { uptime: { pathname: '/app/uptime', }, + apm: { + pathname: '/app/apm' + }, + ml: { + pathname: '/app/ml' + }, rollupJob: { pathname: '/app/kibana', hash: '/management/elasticsearch/rollup_jobs/', }, + apm: { + pathname: '/app/apm', + } }, // choose where esArchiver should load archives from diff --git a/x-pack/test/functional/es_archives/canvas/default/mappings.json b/x-pack/test/functional/es_archives/canvas/default/mappings.json index ba7691797e32..3bde3969e5de 100644 --- a/x-pack/test/functional/es_archives/canvas/default/mappings.json +++ b/x-pack/test/functional/es_archives/canvas/default/mappings.json @@ -224,6 +224,9 @@ "initials": { "type": "keyword" }, + "disabledFeatures": { + "type": "keyword" + }, "name": { "fields": { "keyword": { diff --git a/x-pack/test/functional/es_archives/dashboard/feature_controls/security/data.json b/x-pack/test/functional/es_archives/dashboard/feature_controls/security/data.json new file mode 100644 index 000000000000..f085bad4c507 --- /dev/null +++ b/x-pack/test/functional/es_archives/dashboard/feature_controls/security/data.json @@ -0,0 +1,85 @@ +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "index-pattern:logstash-*", + "source": { + "index-pattern": { + "title": "logstash-*", + "timeFieldName": "@timestamp", + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" + }, + "type": "index-pattern", + "migrationVersion": { + "index-pattern": "6.5.0" + }, + "updated_at": "2018-12-21T00:43:07.096Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "visualization:75c3e060-1e7c-11e9-8488-65449e65d0ed", + "source": { + "visualization": { + "title": "A Pie", + "visState": "{\"title\":\"A Pie\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100},\"dimensions\":{\"metric\":{\"accessor\":0,\"format\":{\"id\":\"number\"},\"params\":{},\"aggType\":\"count\"}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.src\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}", + "uiStateJSON": "{}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"logstash-*\",\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" + } + }, + "type": "visualization", + "updated_at": "2019-01-22T19:32:31.206Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "dashboard:i-exist", + "source": { + "dashboard": { + "title": "A Dashboard", + "hits": 0, + "description": "", + "panelsJSON": "[{\"gridData\":{\"w\":24,\"h\":15,\"x\":0,\"y\":0,\"i\":\"1\"},\"version\":\"7.0.0\",\"panelIndex\":\"1\",\"type\":\"visualization\",\"id\":\"75c3e060-1e7c-11e9-8488-65449e65d0ed\",\"embeddableConfig\":{}}]", + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "version": 1, + "timeRestore": false, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" + } + }, + "type": "dashboard", + "updated_at": "2019-01-22T19:32:47.232Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "config:6.0.0", + "source": { + "config": { + "buildNum": 9007199254740991, + "defaultIndex": "logstash-*" + }, + "type": "config", + "updated_at": "2019-01-22T19:32:02.235Z" + } + } +} diff --git a/x-pack/test/functional/es_archives/dashboard/feature_controls/security/mappings.json b/x-pack/test/functional/es_archives/dashboard/feature_controls/security/mappings.json new file mode 100644 index 000000000000..2956597f5cf1 --- /dev/null +++ b/x-pack/test/functional/es_archives/dashboard/feature_controls/security/mappings.json @@ -0,0 +1,461 @@ +{ + "type": "index", + "value": { + "index": ".kibana", + "settings": { + "index": { + "number_of_shards": "1", + "auto_expand_replicas": "0-1", + "number_of_replicas": "0" + } + }, + "mappings": { + "doc": { + "dynamic": "strict", + "properties": { + "apm-telemetry": { + "properties": { + "has_any_services": { + "type": "boolean" + }, + "services_per_agent": { + "properties": { + "go": { + "type": "long", + "null_value": 0 + }, + "java": { + "type": "long", + "null_value": 0 + }, + "js-base": { + "type": "long", + "null_value": 0 + }, + "nodejs": { + "type": "long", + "null_value": 0 + }, + "python": { + "type": "long", + "null_value": 0 + }, + "ruby": { + "type": "long", + "null_value": 0 + } + } + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "id": { + "type": "text", + "index": false + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "accessibility:disableAnimations": { + "type": "boolean" + }, + "buildNum": { + "type": "keyword" + }, + "dateFormat:tz": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "defaultIndex": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "telemetry:optIn": { + "type": "boolean" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "map" : { + "properties" : { + "bounds" : { + "type" : "geo_shape", + "tree" : "quadtree" + }, + "description" : { + "type" : "text" + }, + "layerListJSON" : { + "type" : "text" + }, + "mapStateJSON" : { + "type" : "text" + }, + "title" : { + "type" : "text" + }, + "uiStateJSON" : { + "type" : "text" + }, + "version" : { + "type" : "integer" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "index-pattern": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "space": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "initials": { + "type": "keyword" + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "spaceId": { + "type": "keyword" + }, + "telemetry": { + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + } + } + } +} diff --git a/x-pack/test/functional/es_archives/dashboard/feature_controls/spaces/data.json b/x-pack/test/functional/es_archives/dashboard/feature_controls/spaces/data.json new file mode 100644 index 000000000000..a43c72659b40 --- /dev/null +++ b/x-pack/test/functional/es_archives/dashboard/feature_controls/spaces/data.json @@ -0,0 +1,127 @@ +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "index-pattern:logstash-*", + "source": { + "index-pattern": { + "title": "logstash-*", + "timeFieldName": "@timestamp", + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" + }, + "type": "index-pattern", + "migrationVersion": { + "index-pattern": "6.5.0" + }, + "updated_at": "2018-12-21T00:43:07.096Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "config:6.0.0", + "source": { + "config": { + "buildNum": 9007199254740991, + "defaultIndex": "logstash-*" + }, + "type": "config", + "updated_at": "2019-01-22T19:32:02.235Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "custom_space:index-pattern:logstash-*", + "source": { + "namespace": "custom_space", + "index-pattern": { + "title": "logstash-*", + "timeFieldName": "@timestamp", + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" + }, + "type": "index-pattern", + "migrationVersion": { + "index-pattern": "6.5.0" + }, + "updated_at": "2018-12-21T00:43:07.096Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "custom_space:visualization:75c3e060-1e7c-11e9-8488-65449e65d0ed", + "source": { + "namespace": "custom_space", + "visualization": { + "title": "A Pie", + "visState": "{\"title\":\"A Pie\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100},\"dimensions\":{\"metric\":{\"accessor\":0,\"format\":{\"id\":\"number\"},\"params\":{},\"aggType\":\"count\"}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.src\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}", + "uiStateJSON": "{}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"logstash-*\",\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" + } + }, + "type": "visualization", + "updated_at": "2019-01-22T19:32:31.206Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "custom_space:dashboard:i-exist", + "source": { + "namespace": "custom_space", + "dashboard": { + "title": "A Dashboard", + "hits": 0, + "description": "", + "panelsJSON": "[{\"gridData\":{\"w\":24,\"h\":15,\"x\":0,\"y\":0,\"i\":\"1\"},\"version\":\"7.0.0\",\"panelIndex\":\"1\",\"type\":\"visualization\",\"id\":\"75c3e060-1e7c-11e9-8488-65449e65d0ed\",\"embeddableConfig\":{}}]", + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "version": 1, + "timeRestore": false, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" + } + }, + "type": "dashboard", + "updated_at": "2019-01-22T19:32:47.232Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "custom_space:config:6.0.0", + "source": { + "namespace": "custom_space", + "config": { + "buildNum": 9007199254740991, + "defaultIndex": "logstash-*" + }, + "type": "config", + "updated_at": "2019-01-22T19:32:02.235Z" + } + } +} diff --git a/x-pack/test/functional/es_archives/dashboard/feature_controls/spaces/mappings.json b/x-pack/test/functional/es_archives/dashboard/feature_controls/spaces/mappings.json new file mode 100644 index 000000000000..35696c187537 --- /dev/null +++ b/x-pack/test/functional/es_archives/dashboard/feature_controls/spaces/mappings.json @@ -0,0 +1,461 @@ +{ + "type": "index", + "value": { + "index": ".kibana", + "settings": { + "index": { + "number_of_shards": "1", + "auto_expand_replicas": "0-1", + "number_of_replicas": "0" + } + }, + "mappings": { + "doc": { + "dynamic": "strict", + "properties": { + "apm-telemetry": { + "properties": { + "has_any_services": { + "type": "boolean" + }, + "services_per_agent": { + "properties": { + "go": { + "type": "long", + "null_value": 0 + }, + "java": { + "type": "long", + "null_value": 0 + }, + "js-base": { + "type": "long", + "null_value": 0 + }, + "nodejs": { + "type": "long", + "null_value": 0 + }, + "python": { + "type": "long", + "null_value": 0 + }, + "ruby": { + "type": "long", + "null_value": 0 + } + } + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "id": { + "type": "text", + "index": false + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "accessibility:disableAnimations": { + "type": "boolean" + }, + "buildNum": { + "type": "keyword" + }, + "dateFormat:tz": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "defaultIndex": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "telemetry:optIn": { + "type": "boolean" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "gis-map" : { + "properties" : { + "bounds" : { + "type" : "geo_shape", + "tree" : "quadtree" + }, + "description" : { + "type" : "text" + }, + "layerListJSON" : { + "type" : "text" + }, + "mapStateJSON" : { + "type" : "text" + }, + "title" : { + "type" : "text" + }, + "uiStateJSON" : { + "type" : "text" + }, + "version" : { + "type" : "integer" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "index-pattern": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "space": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "initials": { + "type": "keyword" + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "spaceId": { + "type": "keyword" + }, + "telemetry": { + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + } + } + } +} diff --git a/x-pack/test/functional/es_archives/dashboard_view_mode/mappings.json b/x-pack/test/functional/es_archives/dashboard_view_mode/mappings.json index adf4050bb88c..3558e89558a5 100644 --- a/x-pack/test/functional/es_archives/dashboard_view_mode/mappings.json +++ b/x-pack/test/functional/es_archives/dashboard_view_mode/mappings.json @@ -175,6 +175,9 @@ "description": { "type": "text" }, + "disabledFeatures": { + "type": "keyword" + }, "initials": { "type": "keyword" }, @@ -299,4 +302,4 @@ } } } -} \ No newline at end of file +} diff --git a/x-pack/test/functional/es_archives/discover/feature_controls/security/data.json b/x-pack/test/functional/es_archives/discover/feature_controls/security/data.json new file mode 100644 index 000000000000..3c0613005c95 --- /dev/null +++ b/x-pack/test/functional/es_archives/discover/feature_controls/security/data.json @@ -0,0 +1,37 @@ +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "index-pattern:logstash-*", + "source": { + "index-pattern": { + "title": "logstash-*", + "timeFieldName": "@timestamp", + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" + }, + "type": "index-pattern", + "migrationVersion": { + "index-pattern": "6.5.0" + }, + "updated_at": "2018-12-21T00:43:07.096Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "config:6.0.0", + "source": { + "config": { + "buildNum": 9007199254740991, + "defaultIndex": "logstash-*" + }, + "type": "config", + "updated_at": "2019-01-22T19:32:02.235Z" + } + } +} diff --git a/x-pack/test/functional/es_archives/discover/feature_controls/security/mappings.json b/x-pack/test/functional/es_archives/discover/feature_controls/security/mappings.json new file mode 100644 index 000000000000..35696c187537 --- /dev/null +++ b/x-pack/test/functional/es_archives/discover/feature_controls/security/mappings.json @@ -0,0 +1,461 @@ +{ + "type": "index", + "value": { + "index": ".kibana", + "settings": { + "index": { + "number_of_shards": "1", + "auto_expand_replicas": "0-1", + "number_of_replicas": "0" + } + }, + "mappings": { + "doc": { + "dynamic": "strict", + "properties": { + "apm-telemetry": { + "properties": { + "has_any_services": { + "type": "boolean" + }, + "services_per_agent": { + "properties": { + "go": { + "type": "long", + "null_value": 0 + }, + "java": { + "type": "long", + "null_value": 0 + }, + "js-base": { + "type": "long", + "null_value": 0 + }, + "nodejs": { + "type": "long", + "null_value": 0 + }, + "python": { + "type": "long", + "null_value": 0 + }, + "ruby": { + "type": "long", + "null_value": 0 + } + } + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "id": { + "type": "text", + "index": false + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "accessibility:disableAnimations": { + "type": "boolean" + }, + "buildNum": { + "type": "keyword" + }, + "dateFormat:tz": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "defaultIndex": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "telemetry:optIn": { + "type": "boolean" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "gis-map" : { + "properties" : { + "bounds" : { + "type" : "geo_shape", + "tree" : "quadtree" + }, + "description" : { + "type" : "text" + }, + "layerListJSON" : { + "type" : "text" + }, + "mapStateJSON" : { + "type" : "text" + }, + "title" : { + "type" : "text" + }, + "uiStateJSON" : { + "type" : "text" + }, + "version" : { + "type" : "integer" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "index-pattern": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "space": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "initials": { + "type": "keyword" + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "spaceId": { + "type": "keyword" + }, + "telemetry": { + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + } + } + } +} diff --git a/x-pack/test/functional/es_archives/discover/feature_controls/spaces/data.json b/x-pack/test/functional/es_archives/discover/feature_controls/spaces/data.json new file mode 100644 index 000000000000..af5aa6d04348 --- /dev/null +++ b/x-pack/test/functional/es_archives/discover/feature_controls/spaces/data.json @@ -0,0 +1,77 @@ +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "index-pattern:logstash-*", + "source": { + "index-pattern": { + "title": "logstash-*", + "timeFieldName": "@timestamp", + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" + }, + "type": "index-pattern", + "migrationVersion": { + "index-pattern": "6.5.0" + }, + "updated_at": "2018-12-21T00:43:07.096Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "config:6.0.0", + "source": { + "config": { + "buildNum": 9007199254740991, + "defaultIndex": "logstash-*" + }, + "type": "config", + "updated_at": "2019-01-22T19:32:02.235Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "custom_space:index-pattern:logstash-*", + "source": { + "namespace": "custom_space", + "index-pattern": { + "title": "logstash-*", + "timeFieldName": "@timestamp", + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" + }, + "type": "index-pattern", + "migrationVersion": { + "index-pattern": "6.5.0" + }, + "updated_at": "2018-12-21T00:43:07.096Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "custom_space:config:6.0.0", + "source": { + "namespace": "custom_space", + "config": { + "buildNum": 9007199254740991, + "defaultIndex": "logstash-*" + }, + "type": "config", + "updated_at": "2019-01-22T19:32:02.235Z" + } + } +} diff --git a/x-pack/test/functional/es_archives/discover/feature_controls/spaces/mappings.json b/x-pack/test/functional/es_archives/discover/feature_controls/spaces/mappings.json new file mode 100644 index 000000000000..35696c187537 --- /dev/null +++ b/x-pack/test/functional/es_archives/discover/feature_controls/spaces/mappings.json @@ -0,0 +1,461 @@ +{ + "type": "index", + "value": { + "index": ".kibana", + "settings": { + "index": { + "number_of_shards": "1", + "auto_expand_replicas": "0-1", + "number_of_replicas": "0" + } + }, + "mappings": { + "doc": { + "dynamic": "strict", + "properties": { + "apm-telemetry": { + "properties": { + "has_any_services": { + "type": "boolean" + }, + "services_per_agent": { + "properties": { + "go": { + "type": "long", + "null_value": 0 + }, + "java": { + "type": "long", + "null_value": 0 + }, + "js-base": { + "type": "long", + "null_value": 0 + }, + "nodejs": { + "type": "long", + "null_value": 0 + }, + "python": { + "type": "long", + "null_value": 0 + }, + "ruby": { + "type": "long", + "null_value": 0 + } + } + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "id": { + "type": "text", + "index": false + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "accessibility:disableAnimations": { + "type": "boolean" + }, + "buildNum": { + "type": "keyword" + }, + "dateFormat:tz": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "defaultIndex": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "telemetry:optIn": { + "type": "boolean" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "gis-map" : { + "properties" : { + "bounds" : { + "type" : "geo_shape", + "tree" : "quadtree" + }, + "description" : { + "type" : "text" + }, + "layerListJSON" : { + "type" : "text" + }, + "mapStateJSON" : { + "type" : "text" + }, + "title" : { + "type" : "text" + }, + "uiStateJSON" : { + "type" : "text" + }, + "version" : { + "type" : "integer" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "index-pattern": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "space": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "initials": { + "type": "keyword" + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "spaceId": { + "type": "keyword" + }, + "telemetry": { + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + } + } + } +} diff --git a/x-pack/test/functional/es_archives/empty_kibana/mappings.json b/x-pack/test/functional/es_archives/empty_kibana/mappings.json index 35e3a46a07bb..77eac534850a 100644 --- a/x-pack/test/functional/es_archives/empty_kibana/mappings.json +++ b/x-pack/test/functional/es_archives/empty_kibana/mappings.json @@ -154,6 +154,9 @@ "description": { "type": "text" }, + "disabledFeatures": { + "type": "keyword" + }, "initials": { "type": "keyword" }, @@ -278,4 +281,4 @@ } } } -} \ No newline at end of file +} diff --git a/x-pack/test/functional/es_archives/logstash/empty/mappings.json b/x-pack/test/functional/es_archives/logstash/empty/mappings.json index 9e56ff8a7a43..096c68aefcc3 100644 --- a/x-pack/test/functional/es_archives/logstash/empty/mappings.json +++ b/x-pack/test/functional/es_archives/logstash/empty/mappings.json @@ -186,6 +186,9 @@ "description": { "type": "text" }, + "disabledFeatures": { + "type": "keyword" + }, "initials": { "type": "keyword" }, @@ -310,4 +313,4 @@ } } } -} \ No newline at end of file +} diff --git a/x-pack/test/functional/es_archives/maps/kibana/mappings.json b/x-pack/test/functional/es_archives/maps/kibana/mappings.json index e60e5440d9a2..c7e786b20ac1 100644 --- a/x-pack/test/functional/es_archives/maps/kibana/mappings.json +++ b/x-pack/test/functional/es_archives/maps/kibana/mappings.json @@ -279,6 +279,9 @@ "initials": { "type": "keyword" }, + "disabledFeatures": { + "type": "keyword" + }, "name": { "fields": { "keyword": { diff --git a/x-pack/test/functional/es_archives/saved_objects_management/feature_controls/security/data.json b/x-pack/test/functional/es_archives/saved_objects_management/feature_controls/security/data.json new file mode 100644 index 000000000000..f085bad4c507 --- /dev/null +++ b/x-pack/test/functional/es_archives/saved_objects_management/feature_controls/security/data.json @@ -0,0 +1,85 @@ +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "index-pattern:logstash-*", + "source": { + "index-pattern": { + "title": "logstash-*", + "timeFieldName": "@timestamp", + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" + }, + "type": "index-pattern", + "migrationVersion": { + "index-pattern": "6.5.0" + }, + "updated_at": "2018-12-21T00:43:07.096Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "visualization:75c3e060-1e7c-11e9-8488-65449e65d0ed", + "source": { + "visualization": { + "title": "A Pie", + "visState": "{\"title\":\"A Pie\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100},\"dimensions\":{\"metric\":{\"accessor\":0,\"format\":{\"id\":\"number\"},\"params\":{},\"aggType\":\"count\"}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.src\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}", + "uiStateJSON": "{}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"logstash-*\",\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" + } + }, + "type": "visualization", + "updated_at": "2019-01-22T19:32:31.206Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "dashboard:i-exist", + "source": { + "dashboard": { + "title": "A Dashboard", + "hits": 0, + "description": "", + "panelsJSON": "[{\"gridData\":{\"w\":24,\"h\":15,\"x\":0,\"y\":0,\"i\":\"1\"},\"version\":\"7.0.0\",\"panelIndex\":\"1\",\"type\":\"visualization\",\"id\":\"75c3e060-1e7c-11e9-8488-65449e65d0ed\",\"embeddableConfig\":{}}]", + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "version": 1, + "timeRestore": false, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" + } + }, + "type": "dashboard", + "updated_at": "2019-01-22T19:32:47.232Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "config:6.0.0", + "source": { + "config": { + "buildNum": 9007199254740991, + "defaultIndex": "logstash-*" + }, + "type": "config", + "updated_at": "2019-01-22T19:32:02.235Z" + } + } +} diff --git a/x-pack/test/functional/es_archives/saved_objects_management/feature_controls/security/mappings.json b/x-pack/test/functional/es_archives/saved_objects_management/feature_controls/security/mappings.json new file mode 100644 index 000000000000..2956597f5cf1 --- /dev/null +++ b/x-pack/test/functional/es_archives/saved_objects_management/feature_controls/security/mappings.json @@ -0,0 +1,461 @@ +{ + "type": "index", + "value": { + "index": ".kibana", + "settings": { + "index": { + "number_of_shards": "1", + "auto_expand_replicas": "0-1", + "number_of_replicas": "0" + } + }, + "mappings": { + "doc": { + "dynamic": "strict", + "properties": { + "apm-telemetry": { + "properties": { + "has_any_services": { + "type": "boolean" + }, + "services_per_agent": { + "properties": { + "go": { + "type": "long", + "null_value": 0 + }, + "java": { + "type": "long", + "null_value": 0 + }, + "js-base": { + "type": "long", + "null_value": 0 + }, + "nodejs": { + "type": "long", + "null_value": 0 + }, + "python": { + "type": "long", + "null_value": 0 + }, + "ruby": { + "type": "long", + "null_value": 0 + } + } + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "id": { + "type": "text", + "index": false + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "accessibility:disableAnimations": { + "type": "boolean" + }, + "buildNum": { + "type": "keyword" + }, + "dateFormat:tz": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "defaultIndex": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "telemetry:optIn": { + "type": "boolean" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "map" : { + "properties" : { + "bounds" : { + "type" : "geo_shape", + "tree" : "quadtree" + }, + "description" : { + "type" : "text" + }, + "layerListJSON" : { + "type" : "text" + }, + "mapStateJSON" : { + "type" : "text" + }, + "title" : { + "type" : "text" + }, + "uiStateJSON" : { + "type" : "text" + }, + "version" : { + "type" : "integer" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "index-pattern": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "space": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "initials": { + "type": "keyword" + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "spaceId": { + "type": "keyword" + }, + "telemetry": { + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + } + } + } +} diff --git a/x-pack/test/functional/es_archives/discover/data.json.gz b/x-pack/test/functional/es_archives/security/discover/data.json.gz similarity index 100% rename from x-pack/test/functional/es_archives/discover/data.json.gz rename to x-pack/test/functional/es_archives/security/discover/data.json.gz diff --git a/x-pack/test/functional/es_archives/discover/mappings.json b/x-pack/test/functional/es_archives/security/discover/mappings.json similarity index 98% rename from x-pack/test/functional/es_archives/discover/mappings.json rename to x-pack/test/functional/es_archives/security/discover/mappings.json index adf4050bb88c..3558e89558a5 100644 --- a/x-pack/test/functional/es_archives/discover/mappings.json +++ b/x-pack/test/functional/es_archives/security/discover/mappings.json @@ -175,6 +175,9 @@ "description": { "type": "text" }, + "disabledFeatures": { + "type": "keyword" + }, "initials": { "type": "keyword" }, @@ -299,4 +302,4 @@ } } } -} \ No newline at end of file +} diff --git a/x-pack/test/functional/es_archives/spaces/disabled_features/data.json b/x-pack/test/functional/es_archives/spaces/disabled_features/data.json new file mode 100644 index 000000000000..0826c7dcf164 --- /dev/null +++ b/x-pack/test/functional/es_archives/spaces/disabled_features/data.json @@ -0,0 +1,113 @@ +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "space:default", + "source": { + "space": { + "name": "Default", + "description": "This is the default space!", + "disabledFeatures": [], + "_reserved": true + }, + "type": "space", + "migrationVersion": { + "space": "6.6.0" + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "index-pattern:logstash-*", + "source": { + "index-pattern": { + "title": "logstash-*", + "timeFieldName": "@timestamp", + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" + }, + "type": "index-pattern", + "migrationVersion": { + "index-pattern": "6.5.0" + }, + "updated_at": "2018-12-21T00:43:07.096Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "custom_space:index-pattern:logstash-*", + "source": { + "namespace": "custom_space", + "index-pattern": { + "title": "logstash-*", + "timeFieldName": "@timestamp", + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" + }, + "type": "index-pattern", + "migrationVersion": { + "index-pattern": "6.5.0" + }, + "updated_at": "2018-12-21T00:43:07.096Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "custom_space:visualization:75c3e060-1e7c-11e9-8488-65449e65d0ed", + "source": { + "namespace": "custom_space", + "visualization": { + "title": "A Pie", + "visState": "{\"title\":\"A Pie\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100},\"dimensions\":{\"metric\":{\"accessor\":0,\"format\":{\"id\":\"number\"},\"params\":{},\"aggType\":\"count\"}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.src\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}", + "uiStateJSON": "{}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"logstash-*\",\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" + } + }, + "type": "visualization", + "updated_at": "2019-01-22T19:32:31.206Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "custom_space:dashboard:i-exist", + "source": { + "namespace": "custom_space", + "dashboard": { + "title": "A Dashboard", + "hits": 0, + "description": "", + "panelsJSON": "[{\"gridData\":{\"w\":24,\"h\":15,\"x\":0,\"y\":0,\"i\":\"1\"},\"version\":\"7.0.0\",\"panelIndex\":\"1\",\"type\":\"visualization\",\"id\":\"75c3e060-1e7c-11e9-8488-65449e65d0ed\",\"embeddableConfig\":{}}]", + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "version": 1, + "timeRestore": false, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" + } + }, + "type": "dashboard", + "updated_at": "2019-01-22T19:32:47.232Z" + } + } +} diff --git a/x-pack/test/functional/es_archives/spaces/disabled_features/mappings.json b/x-pack/test/functional/es_archives/spaces/disabled_features/mappings.json new file mode 100644 index 000000000000..2956597f5cf1 --- /dev/null +++ b/x-pack/test/functional/es_archives/spaces/disabled_features/mappings.json @@ -0,0 +1,461 @@ +{ + "type": "index", + "value": { + "index": ".kibana", + "settings": { + "index": { + "number_of_shards": "1", + "auto_expand_replicas": "0-1", + "number_of_replicas": "0" + } + }, + "mappings": { + "doc": { + "dynamic": "strict", + "properties": { + "apm-telemetry": { + "properties": { + "has_any_services": { + "type": "boolean" + }, + "services_per_agent": { + "properties": { + "go": { + "type": "long", + "null_value": 0 + }, + "java": { + "type": "long", + "null_value": 0 + }, + "js-base": { + "type": "long", + "null_value": 0 + }, + "nodejs": { + "type": "long", + "null_value": 0 + }, + "python": { + "type": "long", + "null_value": 0 + }, + "ruby": { + "type": "long", + "null_value": 0 + } + } + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "id": { + "type": "text", + "index": false + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "accessibility:disableAnimations": { + "type": "boolean" + }, + "buildNum": { + "type": "keyword" + }, + "dateFormat:tz": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "defaultIndex": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "telemetry:optIn": { + "type": "boolean" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "map" : { + "properties" : { + "bounds" : { + "type" : "geo_shape", + "tree" : "quadtree" + }, + "description" : { + "type" : "text" + }, + "layerListJSON" : { + "type" : "text" + }, + "mapStateJSON" : { + "type" : "text" + }, + "title" : { + "type" : "text" + }, + "uiStateJSON" : { + "type" : "text" + }, + "version" : { + "type" : "integer" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "index-pattern": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "space": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "initials": { + "type": "keyword" + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "spaceId": { + "type": "keyword" + }, + "telemetry": { + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + } + } + } +} diff --git a/x-pack/test/functional/es_archives/spaces/data.json b/x-pack/test/functional/es_archives/spaces/selector/data.json similarity index 100% rename from x-pack/test/functional/es_archives/spaces/data.json rename to x-pack/test/functional/es_archives/spaces/selector/data.json diff --git a/x-pack/test/functional/es_archives/spaces/mappings.json b/x-pack/test/functional/es_archives/spaces/selector/mappings.json similarity index 98% rename from x-pack/test/functional/es_archives/spaces/mappings.json rename to x-pack/test/functional/es_archives/spaces/selector/mappings.json index 35e3a46a07bb..77eac534850a 100644 --- a/x-pack/test/functional/es_archives/spaces/mappings.json +++ b/x-pack/test/functional/es_archives/spaces/selector/mappings.json @@ -154,6 +154,9 @@ "description": { "type": "text" }, + "disabledFeatures": { + "type": "keyword" + }, "initials": { "type": "keyword" }, @@ -278,4 +281,4 @@ } } } -} \ No newline at end of file +} diff --git a/x-pack/test/functional/es_archives/timelion/feature_controls/data.json b/x-pack/test/functional/es_archives/timelion/feature_controls/data.json new file mode 100644 index 000000000000..03fc0d57e927 --- /dev/null +++ b/x-pack/test/functional/es_archives/timelion/feature_controls/data.json @@ -0,0 +1,81 @@ +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "config:7.0.0", + "source": { + "config": { + "buildNum": 9007199254740991, + "defaultIndex": "logstash-*" + }, + "type": "config", + "updated_at": "2019-01-22T19:32:02.235Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "id": "index-pattern:logstash-*", + "source": { + "type": "index-pattern", + "index-pattern": { + "title": "logstash-*", + "timeFieldName": "@timestamp", + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"kilobytes\",\"type\":\"number\",\"count\":0,\"scripted\":true,\"script\":\"doc['bytes'].value / 1000\",\"lang\":\"painless\",\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"machine os raw\",\"type\":\"string\",\"count\":0,\"scripted\":true,\"script\":\"doc['machine.os.raw'].value\",\"lang\":\"painless\",\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false}]" + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "id": "timelion-sheet:i-exist", + "source": { + "timelion-sheet": { + "title": "i-exist", + "hits": 0, + "description": "", + "timelion_sheet": [ + ".es(*)" + ], + "timelion_interval": "auto", + "timelion_chart_height": 275, + "timelion_columns": 2, + "timelion_rows": 2, + "version": 1 + }, + "type": "timelion-sheet" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "id": "custom_space:timelion-sheet:i-exist", + "source": { + "namespace": "custom_space", + "timelion-sheet": { + "title": "i-exist", + "hits": 0, + "description": "", + "timelion_sheet": [ + ".es(*).label('custom space sheet')" + ], + "timelion_interval": "auto", + "timelion_chart_height": 275, + "timelion_columns": 2, + "timelion_rows": 2, + "version": 1 + }, + "type": "timelion-sheet" + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/timelion/feature_controls/mappings.json b/x-pack/test/functional/es_archives/timelion/feature_controls/mappings.json new file mode 100644 index 000000000000..2956597f5cf1 --- /dev/null +++ b/x-pack/test/functional/es_archives/timelion/feature_controls/mappings.json @@ -0,0 +1,461 @@ +{ + "type": "index", + "value": { + "index": ".kibana", + "settings": { + "index": { + "number_of_shards": "1", + "auto_expand_replicas": "0-1", + "number_of_replicas": "0" + } + }, + "mappings": { + "doc": { + "dynamic": "strict", + "properties": { + "apm-telemetry": { + "properties": { + "has_any_services": { + "type": "boolean" + }, + "services_per_agent": { + "properties": { + "go": { + "type": "long", + "null_value": 0 + }, + "java": { + "type": "long", + "null_value": 0 + }, + "js-base": { + "type": "long", + "null_value": 0 + }, + "nodejs": { + "type": "long", + "null_value": 0 + }, + "python": { + "type": "long", + "null_value": 0 + }, + "ruby": { + "type": "long", + "null_value": 0 + } + } + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "id": { + "type": "text", + "index": false + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "accessibility:disableAnimations": { + "type": "boolean" + }, + "buildNum": { + "type": "keyword" + }, + "dateFormat:tz": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "defaultIndex": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "telemetry:optIn": { + "type": "boolean" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "map" : { + "properties" : { + "bounds" : { + "type" : "geo_shape", + "tree" : "quadtree" + }, + "description" : { + "type" : "text" + }, + "layerListJSON" : { + "type" : "text" + }, + "mapStateJSON" : { + "type" : "text" + }, + "title" : { + "type" : "text" + }, + "uiStateJSON" : { + "type" : "text" + }, + "version" : { + "type" : "integer" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "index-pattern": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "space": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "initials": { + "type": "keyword" + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "spaceId": { + "type": "keyword" + }, + "telemetry": { + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + } + } + } +} diff --git a/x-pack/test/functional/es_archives/visualize/default/data.json b/x-pack/test/functional/es_archives/visualize/default/data.json new file mode 100644 index 000000000000..250af4d5c5c1 --- /dev/null +++ b/x-pack/test/functional/es_archives/visualize/default/data.json @@ -0,0 +1,110 @@ +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "space:default", + "source": { + "space": { + "name": "Default", + "description": "This is the default space!", + "disabledFeatures": [], + "_reserved": true + }, + "type": "space", + "migrationVersion": { + "space": "6.6.0" + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "index-pattern:logstash-*", + "source": { + "index-pattern": { + "title": "logstash-*", + "timeFieldName": "@timestamp", + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" + }, + "type": "index-pattern", + "migrationVersion": { + "index-pattern": "6.5.0" + }, + "updated_at": "2018-12-21T00:43:07.096Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "custom_space:index-pattern:logstash-*", + "source": { + "namespace": "custom_space", + "index-pattern": { + "title": "logstash-*", + "timeFieldName": "@timestamp", + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" + }, + "type": "index-pattern", + "migrationVersion": { + "index-pattern": "6.5.0" + }, + "updated_at": "2018-12-21T00:43:07.096Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "visualization:i-exist", + "source": { + "visualization": { + "title": "A Pie", + "visState": "{\"title\":\"A Pie\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100},\"dimensions\":{\"metric\":{\"accessor\":0,\"format\":{\"id\":\"number\"},\"params\":{},\"aggType\":\"count\"}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.src\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}", + "uiStateJSON": "{}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"logstash-*\",\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" + } + }, + "type": "visualization", + "updated_at": "2019-01-22T19:32:31.206Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "custom_space:visualization:i-exist", + "source": { + "namespace": "custom_space", + "visualization": { + "title": "A Pie", + "visState": "{\"title\":\"A Pie\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100},\"dimensions\":{\"metric\":{\"accessor\":0,\"format\":{\"id\":\"number\"},\"params\":{},\"aggType\":\"count\"}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.src\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}", + "uiStateJSON": "{}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"logstash-*\",\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" + } + }, + "type": "visualization", + "updated_at": "2019-01-22T19:32:31.206Z" + } + } +} diff --git a/x-pack/test/functional/es_archives/visualize/default/mappings.json b/x-pack/test/functional/es_archives/visualize/default/mappings.json new file mode 100644 index 000000000000..35696c187537 --- /dev/null +++ b/x-pack/test/functional/es_archives/visualize/default/mappings.json @@ -0,0 +1,461 @@ +{ + "type": "index", + "value": { + "index": ".kibana", + "settings": { + "index": { + "number_of_shards": "1", + "auto_expand_replicas": "0-1", + "number_of_replicas": "0" + } + }, + "mappings": { + "doc": { + "dynamic": "strict", + "properties": { + "apm-telemetry": { + "properties": { + "has_any_services": { + "type": "boolean" + }, + "services_per_agent": { + "properties": { + "go": { + "type": "long", + "null_value": 0 + }, + "java": { + "type": "long", + "null_value": 0 + }, + "js-base": { + "type": "long", + "null_value": 0 + }, + "nodejs": { + "type": "long", + "null_value": 0 + }, + "python": { + "type": "long", + "null_value": 0 + }, + "ruby": { + "type": "long", + "null_value": 0 + } + } + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "id": { + "type": "text", + "index": false + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "accessibility:disableAnimations": { + "type": "boolean" + }, + "buildNum": { + "type": "keyword" + }, + "dateFormat:tz": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "defaultIndex": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "telemetry:optIn": { + "type": "boolean" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "gis-map" : { + "properties" : { + "bounds" : { + "type" : "geo_shape", + "tree" : "quadtree" + }, + "description" : { + "type" : "text" + }, + "layerListJSON" : { + "type" : "text" + }, + "mapStateJSON" : { + "type" : "text" + }, + "title" : { + "type" : "text" + }, + "uiStateJSON" : { + "type" : "text" + }, + "version" : { + "type" : "integer" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "index-pattern": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "space": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "initials": { + "type": "keyword" + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "spaceId": { + "type": "keyword" + }, + "telemetry": { + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + } + } + } +} diff --git a/x-pack/test/functional/page_objects/canvas_page.ts b/x-pack/test/functional/page_objects/canvas_page.ts new file mode 100644 index 000000000000..69eb3cc36e8f --- /dev/null +++ b/x-pack/test/functional/page_objects/canvas_page.ts @@ -0,0 +1,44 @@ +/* + * 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 { KibanaFunctionalTestDefaultProviders } from '../../types/providers'; + +export function CanvasPageProvider({ getService }: KibanaFunctionalTestDefaultProviders) { + const testSubjects = getService('testSubjects'); + const find = getService('find'); + + return { + async expectCreateWorkpadButtonEnabled() { + const button = await testSubjects.find('create-workpad-button'); + const disabledAttr = await button.getAttribute('disabled'); + expect(disabledAttr).to.be(null); + }, + + async expectCreateWorkpadButtonDisabled() { + const button = await testSubjects.find('create-workpad-button'); + const disabledAttr = await button.getAttribute('disabled'); + expect(disabledAttr).to.be('true'); + }, + + async expectAddElementButton() { + await testSubjects.existOrFail('add-element-button'); + }, + + async expectNoAddElementButton() { + // Ensure page is fully loaded first by waiting for the refresh button + const refreshPopoverExists = await find.existsByCssSelector('#auto-refresh-popover', 20000); + expect(refreshPopoverExists).to.be(true); + + const addElementButtonExists = await find.existsByCssSelector( + 'button[data-test-subj=add-element-button]', + 10 // don't need much of a wait at all here, because we already waited for refresh button above + ); + expect(addElementButtonExists).to.be(false); + }, + }; +} diff --git a/x-pack/test/functional/page_objects/gis_page.js b/x-pack/test/functional/page_objects/gis_page.js index 6d5d763d73a4..f7714b2a62f9 100644 --- a/x-pack/test/functional/page_objects/gis_page.js +++ b/x-pack/test/functional/page_objects/gis_page.js @@ -129,6 +129,22 @@ export function GisPageProvider({ getService, getPageObjects }) { await testSubjects.clickWhenNotDisabled('confirmSaveSavedObjectButton'); } + async expectMissingSaveButton() { + await testSubjects.missingOrFail('mapSaveButton'); + } + + async expectMissingCreateNewButton() { + await testSubjects.missingOrFail('newMapLink'); + } + + async expectMissingAddLayerButton() { + await testSubjects.missingOrFail('addLayerButton'); + } + + async expectExistAddLayerButton() { + await testSubjects.existOrFail('addLayerButton'); + } + async onMapListingPage() { log.debug(`onMapListingPage`); const exists = await testSubjects.exists('mapsListingPage'); diff --git a/x-pack/test/functional/page_objects/index.js b/x-pack/test/functional/page_objects/index.js index 0eb5c77fe11f..571d9c4aef5e 100644 --- a/x-pack/test/functional/page_objects/index.js +++ b/x-pack/test/functional/page_objects/index.js @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +export { CanvasPageProvider } from './canvas_page'; export { SecurityPageProvider } from './security_page'; export { MonitoringPageProvider } from './monitoring_page'; export { LogstashPageProvider } from './logstash_page'; diff --git a/x-pack/test/functional/page_objects/security_page.js b/x-pack/test/functional/page_objects/security_page.js index 1f9ca10ef303..fde1eedbfdf5 100644 --- a/x-pack/test/functional/page_objects/security_page.js +++ b/x-pack/test/functional/page_objects/security_page.js @@ -16,7 +16,7 @@ export function SecurityPageProvider({ getService, getPageObjects }) { const testSubjects = getService('testSubjects'); const esArchiver = getService('esArchiver'); const userMenu = getService('userMenu'); - const PageObjects = getPageObjects(['common', 'header', 'settings', 'home']); + const PageObjects = getPageObjects(['common', 'header', 'settings', 'home', 'error']); class LoginPage { async login(username, password, options = {}) { @@ -27,6 +27,7 @@ export function SecurityPageProvider({ getService, getPageObjects }) { const expectSpaceSelector = options.expectSpaceSelector || false; const expectSuccess = options.expectSuccess; + const expectForbidden = options.expectForbidden || false; await PageObjects.common.navigateToApp('login'); await testSubjects.setValue('loginUsername', username); @@ -37,6 +38,11 @@ export function SecurityPageProvider({ getService, getPageObjects }) { if (expectSpaceSelector) { await retry.try(() => testSubjects.find('kibanaSpaceSelector')); log.debug(`Finished login process, landed on space selector. currentUrl = ${await browser.getCurrentUrl()}`); + } else if (expectForbidden) { + await retry.try(async () => { + await PageObjects.error.expectForbidden(); + }); + log.debug(`Finished login process, found forbidden message. currentUrl = ${await browser.getCurrentUrl()}`); } else if (expectSuccess) { await find.byCssSelector('[data-test-subj="kibanaChrome"] nav:not(.ng-hide) ', 20000); log.debug(`Finished login process currentUrl = ${await browser.getCurrentUrl()}`); @@ -73,7 +79,7 @@ export function SecurityPageProvider({ getService, getPageObjects }) { async login(username, password, options = {}) { await this.loginPage.login(username, password, options); - if (options.expectSpaceSelector) { + if (options.expectSpaceSelector || options.expectForbidden) { return; } @@ -97,6 +103,22 @@ export function SecurityPageProvider({ getService, getPageObjects }) { )); } + async forceLogout() { + log.debug('SecurityPage.forceLogout'); + if (await find.existsByDisplayedByCssSelector('.login-form', 100)) { + log.debug('Already on the login page, not forcing anything'); + return; + } + + log.debug('Redirecting to /logout to force the logout'); + const url = PageObjects.common.getHostPort() + '/logout'; + await browser.get(url); + log.debug('Waiting on the login form to appear'); + await retry.waitForWithTimeout('login form', config.get('timeouts.waitFor') * 5, async () => ( + await find.existsByDisplayedByCssSelector('.login-form') + )); + } + async clickRolesSection() { await testSubjects.click('roles'); } @@ -273,17 +295,27 @@ export function SecurityPageProvider({ getService, getPageObjects }) { function addKibanaPriv(priv) { - return priv.reduce(function (promise, privName) { - // We have to use non-test-subject selectors because this markup is generated by ui-select. - return promise + return priv.reduce(async function (promise, privName) { - .then(async function () { - log.debug('priv item = ' + privName); - return find.byCssSelector(`[data-test-subj="kibanaMinimumPrivilege"] option[value="${privName}"]`); - }) - .then(function (element) { - return element.click(); - }); + const button = await testSubjects.find('addSpacePrivilegeButton'); + await button.click(); + + const spaceSelector = await testSubjects.find('spaceSelectorComboBox'); + await spaceSelector.click(); + + const globalSpaceOption = await find.byCssSelector(`#spaceOption_\\*`); + await globalSpaceOption.click(); + + const basePrivilegeSelector = await testSubjects.find('basePrivilegeComboBox'); + await basePrivilegeSelector.click(); + + const privilegeOption = await find.byCssSelector(`#basePrivilege_${privName}`); + await privilegeOption.click(); + + const createPrivilegeButton = await testSubjects.find('createSpacePrivilegeButton'); + await createPrivilegeButton.click(); + + return promise; }, Promise.resolve()); } diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json index 075951a07164..0e1db569a208 100644 --- a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json +++ b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json @@ -199,6 +199,9 @@ "description": { "type": "text" }, + "disabledFeatures": { + "type": "keyword" + }, "initials": { "type": "keyword" }, @@ -322,4 +325,4 @@ } } } -} \ No newline at end of file +} diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/index.js b/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/index.js index 3fdbb6b9a250..8980bc565a2d 100644 --- a/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/index.js +++ b/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/index.js @@ -8,7 +8,7 @@ import mappings from './mappings.json'; export default function (kibana) { return new kibana.Plugin({ - require: [], + require: ['kibana', 'elasticsearch', 'xpack_main'], name: 'namespace_agnostic_type_plugin', uiExports: { savedObjectSchemas: { @@ -20,5 +20,31 @@ export default function (kibana) { }, config() {}, + + init(server) { + server.plugins.xpack_main.registerFeature({ + id: 'namespace_agnostic_type_plugin', + name: 'namespace_agnostic_type_plugin', + icon: 'upArrow', + navLinkId: 'namespace_agnostic_type_plugin', + app: [], + privileges: { + all: { + savedObject: { + all: ['globaltype'], + read: [], + }, + ui: [], + }, + read: { + savedObject: { + all: [], + read: ['globaltype'], + }, + ui: [], + } + } + }); + } }); } diff --git a/x-pack/test/saved_object_api_integration/common/lib/create_users_and_roles.ts b/x-pack/test/saved_object_api_integration/common/lib/create_users_and_roles.ts index ab284d13f0d0..730e974de43c 100644 --- a/x-pack/test/saved_object_api_integration/common/lib/create_users_and_roles.ts +++ b/x-pack/test/saved_object_api_integration/common/lib/create_users_and_roles.ts @@ -7,88 +7,128 @@ import { SuperTest } from 'supertest'; import { AUTHENTICATION } from './authentication'; export const createUsersAndRoles = async (es: any, supertest: SuperTest) => { - await supertest.put('/api/security/role/kibana_legacy_user').send({ - elasticsearch: { - indices: [ + await supertest + .put('/api/security/role/kibana_legacy_user') + .send({ + elasticsearch: { + indices: [ + { + names: ['.kibana'], + privileges: ['manage', 'read', 'index', 'delete'], + }, + ], + }, + }) + .expect(204); + + await supertest + .put('/api/security/role/kibana_dual_privileges_user') + .send({ + elasticsearch: { + indices: [ + { + names: ['.kibana'], + privileges: ['manage', 'read', 'index', 'delete'], + }, + ], + }, + kibana: [ { - names: ['.kibana'], - privileges: ['manage', 'read', 'index', 'delete'], + base: ['all'], + spaces: ['*'], }, ], - }, - }); - - await supertest.put('/api/security/role/kibana_dual_privileges_user').send({ - elasticsearch: { - indices: [ + }) + .expect(204); + + await supertest + .put('/api/security/role/kibana_dual_privileges_dashboard_only_user') + .send({ + elasticsearch: { + indices: [ + { + names: ['.kibana'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + kibana: [ { - names: ['.kibana'], - privileges: ['manage', 'read', 'index', 'delete'], + base: ['read'], + spaces: ['*'], }, ], - }, - kibana: { - global: ['all'], - }, + }) + .expect(204); + + await supertest.put('/api/security/role/kibana_rbac_user').send({ + kibana: [ + { + base: ['all'], + spaces: ['*'], + }, + ], }); - await supertest.put('/api/security/role/kibana_dual_privileges_dashboard_only_user').send({ - elasticsearch: { - indices: [ + await supertest + .put('/api/security/role/kibana_rbac_dashboard_only_user') + .send({ + kibana: [ { - names: ['.kibana'], - privileges: ['read', 'view_index_metadata'], + base: ['read'], + spaces: ['*'], }, ], - }, - kibana: { - global: ['read'], - }, - }); - - await supertest.put('/api/security/role/kibana_rbac_user').send({ - kibana: { - global: ['all'], - }, - }); + }) + .expect(204); - await supertest.put('/api/security/role/kibana_rbac_dashboard_only_user').send({ - kibana: { - global: ['read'], - }, - }); - - await supertest.put('/api/security/role/kibana_rbac_default_space_all_user').send({ - kibana: { - space: { - default: ['all'], - }, - }, - }); + await supertest + .put('/api/security/role/kibana_rbac_default_space_all_user') + .send({ + kibana: [ + { + base: ['all'], + spaces: ['default'], + }, + ], + }) + .expect(204); - await supertest.put('/api/security/role/kibana_rbac_default_space_read_user').send({ - kibana: { - space: { - default: ['read'], - }, - }, - }); + await supertest + .put('/api/security/role/kibana_rbac_default_space_read_user') + .send({ + kibana: [ + { + base: ['read'], + spaces: ['default'], + }, + ], + }) + .expect(204); - await supertest.put('/api/security/role/kibana_rbac_space_1_all_user').send({ - kibana: { - space: { - space_1: ['all'], - }, - }, - }); + await supertest + .put('/api/security/role/kibana_rbac_space_1_all_user') + .send({ + kibana: [ + { + base: ['all'], + spaces: ['space_1'], + }, + ], + }) + .expect(204); - await supertest.put('/api/security/role/kibana_rbac_space_1_read_user').send({ - kibana: { - space: { - space_1: ['read'], - }, - }, - }); + await supertest + .put('/api/security/role/kibana_rbac_space_1_read_user') + .send({ + kibana: [ + { + base: ['read'], + spaces: ['space_1'], + }, + ], + }) + .expect(204); await es.shield.putUser({ username: AUTHENTICATION.NOT_A_KIBANA_USER.username, diff --git a/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts b/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts index b0390af36017..4e9d367be539 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts @@ -136,7 +136,7 @@ export function bulkCreateTestSuiteFactory(es: any, esArchiver: any, supertest: expect(resp.body).to.eql({ statusCode: 403, error: 'Forbidden', - message: `Unable to bulk_create dashboard,globaltype,visualization, missing action:saved_objects/dashboard/bulk_create,action:saved_objects/globaltype/bulk_create,action:saved_objects/visualization/bulk_create`, + message: `Unable to bulk_create dashboard,globaltype,visualization`, }); }; diff --git a/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts b/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts index 8d25d3cd0c77..2d49d3ecbd6b 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts @@ -79,7 +79,7 @@ export function bulkGetTestSuiteFactory(esArchiver: any, supertest: SuperTest) }; const createExpectRbacForbidden = (type?: string) => (resp: { [key: string]: any }) => { - const message = type - ? `Unable to find ${type}, missing action:saved_objects/${type}/find` - : `Not authorized to find saved_object`; + const message = type ? `Unable to find ${type}` : `Not authorized to find saved_object`; expect(resp.body).to.eql({ statusCode: 403, diff --git a/x-pack/test/saved_object_api_integration/common/suites/get.ts b/x-pack/test/saved_object_api_integration/common/suites/get.ts index aeafe05d0b3c..7e4340862fc7 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/get.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/get.ts @@ -53,7 +53,7 @@ export function getTestSuiteFactory(esArchiver: any, supertest: SuperTest) const createExpectNotSpaceAwareRbacForbidden = () => (resp: { [key: string]: any }) => { expect(resp.body).to.eql({ error: 'Forbidden', - message: `Unable to get globaltype, missing action:saved_objects/globaltype/get`, + message: `Unable to get globaltype`, statusCode: 403, }); }; @@ -76,7 +76,7 @@ export function getTestSuiteFactory(esArchiver: any, supertest: SuperTest) const createExpectRbacForbidden = (type: string) => (resp: { [key: string]: any }) => { expect(resp.body).to.eql({ error: 'Forbidden', - message: `Unable to get ${type}, missing action:saved_objects/${type}/get`, + message: `Unable to get ${type}`, statusCode: 403, }); }; diff --git a/x-pack/test/saved_object_api_integration/common/suites/import.ts b/x-pack/test/saved_object_api_integration/common/suites/import.ts index 2e71d4a50f85..afb84fd5f935 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/import.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/import.ts @@ -77,7 +77,7 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe expect(resp.body).to.eql({ statusCode: 403, error: 'Forbidden', - message: `Unable to bulk_create dashboard,globaltype, missing action:saved_objects/dashboard/bulk_create,action:saved_objects/globaltype/bulk_create`, + message: `Unable to bulk_create dashboard,globaltype`, }); }; @@ -85,7 +85,7 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe expect(resp.body).to.eql({ statusCode: 403, error: 'Forbidden', - message: `Unable to bulk_create dashboard,globaltype,wigwags, missing action:saved_objects/dashboard/bulk_create,action:saved_objects/globaltype/bulk_create,action:saved_objects/wigwags/bulk_create`, + message: `Unable to bulk_create dashboard,globaltype,wigwags`, }); }; @@ -93,7 +93,7 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe expect(resp.body).to.eql({ statusCode: 403, error: 'Forbidden', - message: `Unable to bulk_create dashboard,globaltype,wigwags, missing action:saved_objects/wigwags/bulk_create`, + message: `Unable to bulk_create wigwags`, }); }; diff --git a/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts index 7419de868be5..a1048f251cb4 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts @@ -81,7 +81,7 @@ export function resolveImportErrorsTestSuiteFactory( expect(resp.body).to.eql({ statusCode: 403, error: 'Forbidden', - message: `Unable to bulk_create dashboard, missing action:saved_objects/dashboard/bulk_create`, + message: `Unable to bulk_create dashboard`, }); }; @@ -89,7 +89,7 @@ export function resolveImportErrorsTestSuiteFactory( expect(resp.body).to.eql({ statusCode: 403, error: 'Forbidden', - message: `Unable to bulk_create dashboard,wigwags, missing action:saved_objects/dashboard/bulk_create,action:saved_objects/wigwags/bulk_create`, + message: `Unable to bulk_create dashboard,wigwags`, }); }; @@ -97,7 +97,7 @@ export function resolveImportErrorsTestSuiteFactory( expect(resp.body).to.eql({ statusCode: 403, error: 'Forbidden', - message: `Unable to bulk_create dashboard,wigwags, missing action:saved_objects/wigwags/bulk_create`, + message: `Unable to bulk_create wigwags`, }); }; diff --git a/x-pack/test/saved_object_api_integration/common/suites/update.ts b/x-pack/test/saved_object_api_integration/common/suites/update.ts index e292daa13d12..fd72e4c20e29 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/update.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/update.ts @@ -51,7 +51,7 @@ export function updateTestSuiteFactory(esArchiver: any, supertest: SuperTest) => { - await supertest.put('/api/security/role/kibana_legacy_user').send({ - elasticsearch: { - indices: [ + await supertest + .put('/api/security/role/kibana_legacy_user') + .send({ + elasticsearch: { + indices: [ + { + names: ['.kibana*'], + privileges: ['manage', 'read', 'index', 'delete'], + }, + ], + }, + }) + .expect(204); + + await supertest + .put('/api/security/role/kibana_dual_privileges_user') + .send({ + elasticsearch: { + indices: [ + { + names: ['.kibana*'], + privileges: ['manage', 'read', 'index', 'delete'], + }, + ], + }, + kibana: [ { - names: ['.kibana*'], - privileges: ['manage', 'read', 'index', 'delete'], + base: ['all'], + spaces: ['*'], }, ], - }, - }); + }) + .expect(204); - await supertest.put('/api/security/role/kibana_dual_privileges_user').send({ - elasticsearch: { - indices: [ + await supertest + .put('/api/security/role/kibana_dual_privileges_dashboard_only_user') + .send({ + elasticsearch: { + indices: [ + { + names: ['.kibana*'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + kibana: [ { - names: ['.kibana*'], - privileges: ['manage', 'read', 'index', 'delete'], + base: ['read'], + spaces: ['*'], }, ], - }, - kibana: { - global: ['all'], - }, - }); + }) + .expect(204); - await supertest.put('/api/security/role/kibana_dual_privileges_dashboard_only_user').send({ - elasticsearch: { - indices: [ + await supertest + .put('/api/security/role/kibana_rbac_user') + .send({ + kibana: [ { - names: ['.kibana*'], - privileges: ['read', 'view_index_metadata'], + base: ['all'], + spaces: ['*'], }, ], - }, - kibana: { - global: ['read'], - }, - }); - - await supertest.put('/api/security/role/kibana_rbac_user').send({ - kibana: { - global: ['all'], - }, - }); + }) + .expect(204); - await supertest.put('/api/security/role/kibana_rbac_dashboard_only_user').send({ - kibana: { - global: ['read'], - }, - }); + await supertest + .put('/api/security/role/kibana_rbac_dashboard_only_user') + .send({ + kibana: [ + { + base: ['read'], + spaces: ['*'], + }, + ], + }) + .expect(204); - await supertest.put('/api/security/role/kibana_rbac_default_space_all_user').send({ - kibana: { - space: { - default: ['all'], - }, - }, - }); + await supertest + .put('/api/security/role/kibana_rbac_default_space_all_user') + .send({ + kibana: [ + { + base: ['all'], + spaces: ['default'], + }, + ], + }) + .expect(204); - await supertest.put('/api/security/role/kibana_rbac_default_space_read_user').send({ - kibana: { - space: { - default: ['read'], - }, - }, - }); + await supertest + .put('/api/security/role/kibana_rbac_default_space_read_user') + .send({ + kibana: [ + { + base: ['read'], + spaces: ['default'], + }, + ], + }) + .expect(204); - await supertest.put('/api/security/role/kibana_rbac_space_1_all_user').send({ - kibana: { - space: { - space_1: ['all'], - }, - }, - }); + await supertest + .put('/api/security/role/kibana_rbac_space_1_all_user') + .send({ + kibana: [ + { + base: ['all'], + spaces: ['space_1'], + }, + ], + }) + .expect(204); - await supertest.put('/api/security/role/kibana_rbac_space_1_read_user').send({ - kibana: { - space: { - space_1: ['read'], - }, - }, - }); + await supertest + .put('/api/security/role/kibana_rbac_space_1_read_user') + .send({ + kibana: [ + { + base: ['read'], + spaces: ['space_1'], + }, + ], + }) + .expect(204); - await supertest.put('/api/security/role/kibana_rbac_space_2_all_user').send({ - kibana: { - space: { - space_2: ['all'], - }, - }, - }); + await supertest + .put('/api/security/role/kibana_rbac_space_2_all_user') + .send({ + kibana: [ + { + base: ['all'], + spaces: ['space_2'], + }, + ], + }) + .expect(204); - await supertest.put('/api/security/role/kibana_rbac_space_2_read_user').send({ - kibana: { - space: { - space_2: ['read'], - }, - }, - }); + await supertest + .put('/api/security/role/kibana_rbac_space_2_read_user') + .send({ + kibana: [ + { + base: ['read'], + spaces: ['space_2'], + }, + ], + }) + .expect(204); - await supertest.put('/api/security/role/kibana_rbac_space_1_2_all_user').send({ - kibana: { - space: { - space_1: ['all'], - space_2: ['all'], - }, - }, - }); + await supertest + .put('/api/security/role/kibana_rbac_space_1_2_all_user') + .send({ + kibana: [ + { + base: ['all'], + spaces: ['space_1', 'space_2'], + }, + ], + }) + .expect(204); - await supertest.put('/api/security/role/kibana_rbac_space_1_2_read_user').send({ - kibana: { - space: { - space_1: ['read'], - space_2: ['read'], - }, - }, - }); + await supertest + .put('/api/security/role/kibana_rbac_space_1_2_read_user') + .send({ + kibana: [ + { + base: ['read'], + spaces: ['space_1', 'space_2'], + }, + ], + }) + .expect(204); await es.shield.putUser({ username: AUTHENTICATION.NOT_A_KIBANA_USER.username, @@ -263,4 +320,44 @@ export const createUsersAndRoles = async (es: any, supertest: SuperTest) => email: 'a_kibana_rbac_space_1_2_readonly_user@elastic.co', }, }); + + await es.shield.putUser({ + username: AUTHENTICATION.APM_USER.username, + body: { + password: AUTHENTICATION.APM_USER.password, + roles: ['apm_user'], + full_name: 'a apm user', + email: 'a_apm_user@elastic.co', + }, + }); + + await es.shield.putUser({ + username: AUTHENTICATION.MACHINE_LEARING_ADMIN.username, + body: { + password: AUTHENTICATION.MACHINE_LEARING_ADMIN.password, + roles: ['machine_learning_admin'], + full_name: 'a machine learning admin', + email: 'a_machine_learning_admin@elastic.co', + }, + }); + + await es.shield.putUser({ + username: AUTHENTICATION.MACHINE_LEARNING_USER.username, + body: { + password: AUTHENTICATION.MACHINE_LEARNING_USER.password, + roles: ['machine_learning_user'], + full_name: 'a machine learning user', + email: 'a_machine_learning_user@elastic.co', + }, + }); + + await es.shield.putUser({ + username: AUTHENTICATION.MONITORING_USER.username, + body: { + password: AUTHENTICATION.MONITORING_USER.password, + roles: ['monitoring_user'], + full_name: 'a monitoring user', + email: 'a_monitoring_user@elastic.co', + }, + }); }; diff --git a/x-pack/test/spaces_api_integration/common/suites/create.ts b/x-pack/test/spaces_api_integration/common/suites/create.ts index 6af5afc2530c..4de638c78414 100644 --- a/x-pack/test/spaces_api_integration/common/suites/create.ts +++ b/x-pack/test/spaces_api_integration/common/suites/create.ts @@ -40,6 +40,7 @@ export function createTestSuiteFactory(esArchiver: any, supertest: SuperTest) name: 'Default Space', description: 'This is the default space', _reserved: true, + disabledFeatures: [], }, { id: 'space_1', name: 'Space 1', description: 'This is the first test space', + disabledFeatures: [], }, { id: 'space_2', name: 'Space 2', description: 'This is the second test space', + disabledFeatures: [], }, ]; expect(resp.body).to.eql(allSpaces.find(space => space.id === spaceId)); diff --git a/x-pack/test/spaces_api_integration/common/suites/get_all.ts b/x-pack/test/spaces_api_integration/common/suites/get_all.ts index 64acb60308c4..ea5d391cfa0c 100644 --- a/x-pack/test/spaces_api_integration/common/suites/get_all.ts +++ b/x-pack/test/spaces_api_integration/common/suites/get_all.ts @@ -31,16 +31,19 @@ export function getAllTestSuiteFactory(esArchiver: any, supertest: SuperTest spaceIds.includes(entry.id)); expect(resp.body).to.eql(expectedBody); diff --git a/x-pack/test/spaces_api_integration/common/suites/select.ts b/x-pack/test/spaces_api_integration/common/suites/select.ts index 489dc9f26529..ab8cbfcd0ddc 100644 --- a/x-pack/test/spaces_api_integration/common/suites/select.ts +++ b/x-pack/test/spaces_api_integration/common/suites/select.ts @@ -55,17 +55,20 @@ export function selectTestSuiteFactory(esArchiver: any, supertest: SuperTest space.id === spaceId)); diff --git a/x-pack/test/spaces_api_integration/common/suites/update.ts b/x-pack/test/spaces_api_integration/common/suites/update.ts index 88297df31133..5d64bee0f15a 100644 --- a/x-pack/test/spaces_api_integration/common/suites/update.ts +++ b/x-pack/test/spaces_api_integration/common/suites/update.ts @@ -48,6 +48,7 @@ export function updateTestSuiteFactory(esArchiver: any, supertest: SuperTest { @@ -180,6 +188,50 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) { }, } ); + + getAllTest(`apm_user can't access any spaces from ${scenario.spaceId}`, { + spaceId: scenario.spaceId, + user: scenario.users.apmUser, + tests: { + exists: { + statusCode: 403, + response: expectRbacForbidden, + }, + }, + }); + + getAllTest(`machine_learning_admin can't access any spaces from ${scenario.spaceId}`, { + spaceId: scenario.spaceId, + user: scenario.users.machineLearningAdmin, + tests: { + exists: { + statusCode: 403, + response: expectRbacForbidden, + }, + }, + }); + + getAllTest(`machine_learning_user can't access any spaces from ${scenario.spaceId}`, { + spaceId: scenario.spaceId, + user: scenario.users.machineLearningUser, + tests: { + exists: { + statusCode: 403, + response: expectRbacForbidden, + }, + }, + }); + + getAllTest(`monitoring_user can't access any spaces from ${scenario.spaceId}`, { + spaceId: scenario.spaceId, + user: scenario.users.monitoringUser, + tests: { + exists: { + statusCode: 403, + response: expectRbacForbidden, + }, + }, + }); }); }); } diff --git a/x-pack/test/types/providers.ts b/x-pack/test/types/providers.ts index 2d655b817680..354adccd5f62 100644 --- a/x-pack/test/types/providers.ts +++ b/x-pack/test/types/providers.ts @@ -13,4 +13,5 @@ export interface KibanaFunctionalTestDefaultProviders { getService(serviceName: string): any; getPageObjects(pageObjectNames: string[]): any; loadTestFile(path: string): void; + readConfigFile(path: string): any; } diff --git a/x-pack/test/types/services.d.ts b/x-pack/test/types/services.d.ts new file mode 100644 index 000000000000..a219e43ebd95 --- /dev/null +++ b/x-pack/test/types/services.d.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface LogService { + debug: (message: string) => void; +} diff --git a/x-pack/test/typings/index.d.ts b/x-pack/test/typings/index.d.ts new file mode 100644 index 000000000000..0688ef9e4d8e --- /dev/null +++ b/x-pack/test/typings/index.d.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +declare module '*.html' { + const template: string; + // eslint-disable-next-line import/no-default-export + export default template; +} diff --git a/x-pack/test/ui_capabilities/README.md b/x-pack/test/ui_capabilities/README.md new file mode 100644 index 000000000000..b0f9f8eec7c8 --- /dev/null +++ b/x-pack/test/ui_capabilities/README.md @@ -0,0 +1,48 @@ +# UI Capability Tests +These tests give us the most coverage to ensure that spaces and security work independently and cooperatively. They each cover different situations, and are supplemented by functional UI tests to ensure that security and spaces independently are able to disable the UI elements. These tests are using a "foo" plugin to ensure that its UI capabilities are adjusted appropriately. We aren't using actual plugins/apps for these tests, as they are prone to change and that's not the point of these tests. These tests are to ensure that the primary UI capabilities are adjusted appropriately by both the security and spaces plugins. + +## Security and Spaces + +We want to test for all combinations of the following users at the following spaces. The goal of these tests is to ensure that ui capabilities can be disabled by either the privileges at a specific space, or the space disabling the features. + +### Users +user with no kibana privileges +superuser +legacy all +legacy read +dual privileges all +dual privileges read +global read +global all +everything_space read +everything_space all +nothing_space read +nothing_space all + +### Spaces +everything_space - all features enabled +nothing_space - no features enabled + +## Security + +The security tests focus on more permutations of user's privileges, and focus primarily on privileges granted globally (at all spaces). + +### Users +no kibana privileges +superuser +legacy all +dual privileges all +dual privileges read +global read +global all +foo read +foo all + +## Spaces + +The Space tests focus on the result of disabling certain feature(s). + +### Spaces +everything enabled +nothing enabled +foo disabled diff --git a/x-pack/test/ui_capabilities/common/config.ts b/x-pack/test/ui_capabilities/common/config.ts new file mode 100644 index 000000000000..bf770579ca1b --- /dev/null +++ b/x-pack/test/ui_capabilities/common/config.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import path from 'path'; +import { SecurityServiceProvider, SpacesServiceProvider } from '../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../types/providers'; +import { FeaturesProvider, UICapabilitiesProvider } from './services'; + +interface CreateTestConfigOptions { + license: string; + disabledPlugins?: string[]; +} + +// eslint-disable-next-line import/no-default-export +export function createTestConfig(name: string, options: CreateTestConfigOptions) { + const { license = 'trial', disabledPlugins = [] } = options; + + return async ({ readConfigFile }: KibanaFunctionalTestDefaultProviders) => { + const xPackFunctionalTestsConfig = await readConfigFile( + require.resolve('../../functional/config.js') + ); + + return { + testFiles: [require.resolve(`../${name}/tests/`)], + servers: xPackFunctionalTestsConfig.get('servers'), + services: { + security: SecurityServiceProvider, + spaces: SpacesServiceProvider, + uiCapabilities: UICapabilitiesProvider, + features: FeaturesProvider, + }, + junit: { + reportName: 'X-Pack UI Capabilities Functional Tests', + }, + esArchiver: {}, + esTestCluster: { + ...xPackFunctionalTestsConfig.get('esTestCluster'), + license, + serverArgs: [ + `xpack.license.self_generated.type=${license}`, + `xpack.security.enabled=${!disabledPlugins.includes('security') && license === 'trial'}`, + ], + }, + kbnTestServer: { + ...xPackFunctionalTestsConfig.get('kbnTestServer'), + serverArgs: [ + ...xPackFunctionalTestsConfig.get('kbnTestServer.serverArgs'), + ...disabledPlugins.map(key => `--xpack.${key}.enabled=false`), + `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'foo_plugin')}`, + ], + }, + }; + }; +} diff --git a/x-pack/plugins/security/common/model/kibana_privilege.ts b/x-pack/test/ui_capabilities/common/features.ts similarity index 65% rename from x-pack/plugins/security/common/model/kibana_privilege.ts rename to x-pack/test/ui_capabilities/common/features.ts index 20cac65b4ca7..3c015bc21e93 100644 --- a/x-pack/plugins/security/common/model/kibana_privilege.ts +++ b/x-pack/test/ui_capabilities/common/features.ts @@ -4,6 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -export type KibanaPrivilege = 'none' | 'read' | 'all'; +interface Feature { + navLinkId: string; +} -export const KibanaAppPrivileges: KibanaPrivilege[] = ['read', 'all']; +export interface Features { + [key: string]: Feature; +} diff --git a/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/index.js b/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/index.js new file mode 100644 index 000000000000..07c39d34b130 --- /dev/null +++ b/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/index.js @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export default function (kibana) { + return new kibana.Plugin({ + require: ['kibana', 'elasticsearch', 'xpack_main'], + name: 'foo', + uiExports: { + app: { + title: 'Foo', + order: 1000, + euiIconType: 'uiArray', + description: 'Foo app', + main: 'plugins/foo_plugin/app', + }, + }, + + init(server) { + server.plugins.xpack_main.registerFeature({ + id: 'foo', + name: 'Foo', + icon: 'upArrow', + navLinkId: 'foo_plugin', + app: ['kibana'], + catalogue: ['foo'], + privileges: { + all: { + savedObject: { + all: ['foo'], + read: ['index-pattern', 'config'], + }, + ui: ['create', 'edit', 'delete', 'show'], + }, + read: { + savedObject: { + all: [], + read: ['foo', 'index-pattern', 'config'], + }, + ui: ['show'], + } + } + }); + } + }); +} diff --git a/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/package.json b/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/package.json new file mode 100644 index 000000000000..45228b47c0a3 --- /dev/null +++ b/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/package.json @@ -0,0 +1,7 @@ +{ + "name": "foo_plugin", + "version": "0.0.0", + "kibana": { + "version": "kibana" + } +} diff --git a/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/public/app.js b/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/public/app.js new file mode 100644 index 000000000000..41bc2aa25880 --- /dev/null +++ b/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/public/app.js @@ -0,0 +1,5 @@ +/* + * 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. + */ diff --git a/x-pack/test/ui_capabilities/common/lib/unreachable_error.ts b/x-pack/test/ui_capabilities/common/lib/unreachable_error.ts new file mode 100644 index 000000000000..93e06a89ea05 --- /dev/null +++ b/x-pack/test/ui_capabilities/common/lib/unreachable_error.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +class UnreachableError extends Error { + constructor(val: never) { + super(`Unreachable: ${val}`); + } +} diff --git a/x-pack/test/ui_capabilities/common/nav_links_builder.ts b/x-pack/test/ui_capabilities/common/nav_links_builder.ts new file mode 100644 index 000000000000..8b7741469362 --- /dev/null +++ b/x-pack/test/ui_capabilities/common/nav_links_builder.ts @@ -0,0 +1,44 @@ +/* + * 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 { Features } from './features'; + +type buildCallback = (featureId: string) => boolean; +export class NavLinksBuilder { + private readonly features: Features; + constructor(features: Features) { + this.features = { + ...features, + // management isn't a first-class "feature", but it makes our life easier here to pretend like it is + management: { + navLinkId: 'kibana:management', + }, + }; + } + + public all() { + return this.build(() => true); + } + public except(...feature: string[]) { + return this.build(featureId => !feature.includes(featureId)); + } + public none() { + return this.build(() => false); + } + public only(...feature: string[]) { + return this.build(featureId => feature.includes(featureId)); + } + + private build(callback: buildCallback): Record { + const navLinks = {} as Record; + for (const [featureId, feature] of Object.entries(this.features)) { + if (feature.navLinkId) { + navLinks[feature.navLinkId] = callback(featureId); + } + } + + return navLinks; + } +} diff --git a/x-pack/test/ui_capabilities/common/saved_objects_management_builder.ts b/x-pack/test/ui_capabilities/common/saved_objects_management_builder.ts new file mode 100644 index 000000000000..c522d68180fd --- /dev/null +++ b/x-pack/test/ui_capabilities/common/saved_objects_management_builder.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable max-classes-per-file */ +class SavedObjectsTypeUICapabilitiesGroup { + public all = ['delete', 'edit', 'read']; + public read = ['read']; +} +const savedObjectsTypeUICapabilitiesGroup = new SavedObjectsTypeUICapabilitiesGroup(); + +interface OnlyParameters { + all?: string | string[]; + read?: string | string[]; +} + +const coerceToArray = (itemOrItemsOrNil: T | T[] | undefined): T[] => { + if (itemOrItemsOrNil == null) { + return []; + } + + return Array.isArray(itemOrItemsOrNil) ? itemOrItemsOrNil : [itemOrItemsOrNil]; +}; + +export class SavedObjectsManagementBuilder { + private allSavedObjectTypes: string[]; + + constructor(spacesEnabled: boolean) { + this.allSavedObjectTypes = [ + ...(spacesEnabled ? ['space'] : []), + 'config', + 'telemetry', + 'graph-workspace', + 'ml-telemetry', + 'apm-telemetry', + 'map', + 'maps-telemetry', + 'canvas-workpad', + 'infrastructure-ui-source', + 'upgrade-assistant-reindex-operation', + 'upgrade-assistant-telemetry', + 'index-pattern', + 'visualization', + 'search', + 'dashboard', + 'url', + 'server', + 'kql-telemetry', + 'timelion-sheet', + 'ui-metric', + 'sample-data-telemetry', + ]; + } + + public uiCapabilities(group: keyof SavedObjectsTypeUICapabilitiesGroup) { + return savedObjectsTypeUICapabilitiesGroup.all.reduce( + (acc2, uiCapability) => ({ + ...acc2, + [uiCapability]: savedObjectsTypeUICapabilitiesGroup[group].includes(uiCapability), + }), + {} + ); + } + + public build(parameters: OnlyParameters): Record { + const readTypes = coerceToArray(parameters.read); + const allTypes = coerceToArray(parameters.all); + return this.allSavedObjectTypes.reduce( + (acc, savedObjectType) => ({ + ...acc, + [savedObjectType]: savedObjectsTypeUICapabilitiesGroup.all.reduce( + (acc2, uiCapability) => ({ + ...acc2, + [uiCapability]: + (readTypes.includes(savedObjectType) && + savedObjectsTypeUICapabilitiesGroup.read.includes(uiCapability)) || + (allTypes.includes(savedObjectType) && + savedObjectsTypeUICapabilitiesGroup.all.includes(uiCapability)), + }), + {} + ), + }), + {} + ); + } +} diff --git a/x-pack/test/ui_capabilities/common/services/features.ts b/x-pack/test/ui_capabilities/common/services/features.ts new file mode 100644 index 000000000000..571dd901f1c0 --- /dev/null +++ b/x-pack/test/ui_capabilities/common/services/features.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import axios, { AxiosInstance } from 'axios'; +import { format as formatUrl } from 'url'; +import util from 'util'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; +import { LogService } from '../../../types/services'; +import { Features } from '../features'; + +export class FeaturesService { + private readonly axios: AxiosInstance; + + constructor(url: string, private readonly log: LogService) { + this.axios = axios.create({ + headers: { 'kbn-xsrf': 'x-pack/ftr/services/features' }, + baseURL: url, + maxRedirects: 0, + validateStatus: () => true, // we'll handle our own statusCodes and throw informative errors + }); + } + + public async get(): Promise { + this.log.debug(`requesting /api/features/v1 to get the features`); + const response = await this.axios.get('/api/features/v1'); + + if (response.status !== 200) { + throw new Error( + `Expected status code of 200, received ${response.status} ${ + response.statusText + }: ${util.inspect(response.data)}` + ); + } + + const features = response.data.reduce( + (acc: Features, feature: any) => ({ + ...acc, + [feature.id]: { + navLinkId: feature.navLinkId, + }, + }), + {} + ); + return features; + } +} + +export function FeaturesProvider({ getService }: KibanaFunctionalTestDefaultProviders) { + const log = getService('log'); + const config = getService('config'); + const url = formatUrl(config.get('servers.kibana')); + + return new FeaturesService(url, log); +} diff --git a/x-pack/test/ui_capabilities/common/services/index.ts b/x-pack/test/ui_capabilities/common/services/index.ts new file mode 100644 index 000000000000..9fe948d509ee --- /dev/null +++ b/x-pack/test/ui_capabilities/common/services/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { FeaturesProvider, FeaturesService } from './features'; +export { UICapabilitiesProvider } from './ui_capabilities'; diff --git a/x-pack/test/ui_capabilities/common/services/ui_capabilities.ts b/x-pack/test/ui_capabilities/common/services/ui_capabilities.ts new file mode 100644 index 000000000000..970519e61cc8 --- /dev/null +++ b/x-pack/test/ui_capabilities/common/services/ui_capabilities.ts @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import axios, { AxiosInstance } from 'axios'; +import cheerio from 'cheerio'; +import { UICapabilities } from 'ui/capabilities'; +import { format as formatUrl } from 'url'; +import util from 'util'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; +import { LogService } from '../../../types/services'; + +export interface BasicCredentials { + username: string; + password: string; +} + +export enum GetUICapabilitiesFailureReason { + RedirectedToRoot = 'Redirected to Root', + NotFound = 'Not Found', +} + +interface GetUICapabilitiesResult { + success: boolean; + value?: UICapabilities; + failureReason?: GetUICapabilitiesFailureReason; +} + +export class UICapabilitiesService { + private readonly log: LogService; + private readonly axios: AxiosInstance; + + constructor(url: string, log: LogService) { + this.log = log; + this.axios = axios.create({ + headers: { 'kbn-xsrf': 'x-pack/ftr/services/ui_capabilities' }, + baseURL: url, + maxRedirects: 0, + validateStatus: () => true, // we'll handle our own statusCodes and throw informative errors + }); + } + + public async get( + credentials: BasicCredentials | null, + spaceId?: string + ): Promise { + const spaceUrlPrefix = spaceId ? `/s/${spaceId}` : ''; + this.log.debug(`requesting ${spaceUrlPrefix}/app/kibana to parse the uiCapabilities`); + const requestHeaders = credentials + ? { + Authorization: `Basic ${Buffer.from( + `${credentials.username}:${credentials.password}` + ).toString('base64')}`, + } + : {}; + const response = await this.axios.get(`${spaceUrlPrefix}/app/kibana`, { + headers: requestHeaders, + }); + + if (response.status === 302 && response.headers.location === '/') { + return { + success: false, + failureReason: GetUICapabilitiesFailureReason.RedirectedToRoot, + }; + } + + if (response.status === 404) { + return { + success: false, + failureReason: GetUICapabilitiesFailureReason.NotFound, + }; + } + + if (response.status !== 200) { + throw new Error( + `Expected status code of 200, received ${response.status} ${ + response.statusText + }: ${util.inspect(response.data)}` + ); + } + + const dom = cheerio.load(response.data.toString()); + const element = dom('kbn-injected-metadata'); + if (!element) { + throw new Error('Unable to find "kbn-injected-metadata" element '); + } + + const dataAttrJson = element.attr('data'); + + try { + const dataAttr = JSON.parse(dataAttrJson); + return { + success: true, + value: dataAttr.vars.uiCapabilities as UICapabilities, + }; + } catch (err) { + throw new Error( + `Unable to parse JSON from the kbn-injected-metadata data attribute: ${dataAttrJson}` + ); + } + } +} + +export function UICapabilitiesProvider({ getService }: KibanaFunctionalTestDefaultProviders) { + const log = getService('log'); + const config = getService('config'); + const noAuthUrl = formatUrl({ + ...config.get('servers.kibana'), + auth: undefined, + }); + + return new UICapabilitiesService(noAuthUrl, log); +} diff --git a/x-pack/test/ui_capabilities/common/types.ts b/x-pack/test/ui_capabilities/common/types.ts new file mode 100644 index 000000000000..d7f34b9ecff0 --- /dev/null +++ b/x-pack/test/ui_capabilities/common/types.ts @@ -0,0 +1,56 @@ +/* + * 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. + */ +interface FeaturesPrivileges { + [featureId: string]: string[]; +} + +// TODO: Consolidate the following type definitions +interface CustomRoleSpecificationElasticsearchIndices { + names: string[]; + privileges: string[]; +} + +export interface RoleKibanaPrivilege { + spaces: string[]; + base?: string[]; + feature?: FeaturesPrivileges; +} + +export interface CustomRoleSpecification { + name: string; + elasticsearch?: { + cluster: string[]; + indices: CustomRoleSpecificationElasticsearchIndices[]; + }; + kibana?: RoleKibanaPrivilege[]; +} + +interface ReservedRoleSpecification { + name: string; +} + +export function isCustomRoleSpecification( + roleSpecification: CustomRoleSpecification | ReservedRoleSpecification +): roleSpecification is CustomRoleSpecification { + const customRoleDefinition = roleSpecification as CustomRoleSpecification; + return ( + customRoleDefinition.kibana !== undefined || customRoleDefinition.elasticsearch !== undefined + ); +} + +export interface User { + username: string; + fullName: string; + password: string; + role?: ReservedRoleSpecification | CustomRoleSpecification; + roles?: Array; +} + +export interface Space { + id: string; + name: string; + disabledFeatures: string[] | '*'; +} diff --git a/x-pack/test/ui_capabilities/security_and_spaces/config.ts b/x-pack/test/ui_capabilities/security_and_spaces/config.ts new file mode 100644 index 000000000000..9fdbf8f47baa --- /dev/null +++ b/x-pack/test/ui_capabilities/security_and_spaces/config.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. + */ + +import { createTestConfig } from '../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('security_and_spaces', { disabledPlugins: [], license: 'trial' }); diff --git a/x-pack/test/ui_capabilities/security_and_spaces/scenarios.ts b/x-pack/test/ui_capabilities/security_and_spaces/scenarios.ts new file mode 100644 index 000000000000..8e1289df364c --- /dev/null +++ b/x-pack/test/ui_capabilities/security_and_spaces/scenarios.ts @@ -0,0 +1,468 @@ +/* + * 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 { Space, User } from '../common/types'; + +const NoKibanaPrivileges: User = { + username: 'no_kibana_privileges', + fullName: 'no_kibana_privileges', + password: 'no_kibana_privileges-password', + role: { + name: 'no_kibana_privileges', + elasticsearch: { + indices: [ + { + names: ['foo'], + privileges: ['all'], + }, + ], + }, + }, +}; + +const Superuser: User = { + username: 'superuser', + fullName: 'superuser', + password: 'superuser-password', + role: { + name: 'superuser', + }, +}; + +const LegacyAll: User = { + username: 'legacy_all', + fullName: 'legacy_all', + password: 'legacy_all-password', + role: { + name: 'legacy_all_role', + elasticsearch: { + indices: [ + { + names: ['.kibana*'], + privileges: ['all'], + }, + ], + }, + }, +}; + +const DualPrivilegesAll: User = { + username: 'dual_privileges_all', + fullName: 'dual_privileges_all', + password: 'dual_privileges_all-password', + role: { + name: 'dual_privileges_all_role', + elasticsearch: { + indices: [ + { + names: ['.kibana*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + base: ['all'], + spaces: ['*'], + }, + ], + }, +}; + +const DualPrivilegesRead: User = { + username: 'dual_privileges_read', + fullName: 'dual_privileges_read', + password: 'dual_privileges_read-password', + role: { + name: 'dual_privileges_read_role', + elasticsearch: { + indices: [ + { + names: ['.kibana*'], + privileges: ['read'], + }, + ], + }, + kibana: [ + { + base: ['read'], + spaces: ['*'], + }, + ], + }, +}; + +const GlobalAll: User = { + username: 'global_all', + fullName: 'global_all', + password: 'global_all-password', + role: { + name: 'global_all_role', + kibana: [ + { + base: ['all'], + spaces: ['*'], + }, + ], + }, +}; + +const GlobalRead: User = { + username: 'global_read', + fullName: 'global_read', + password: 'global_read-password', + role: { + name: 'global_read_role', + kibana: [ + { + base: ['read'], + spaces: ['*'], + }, + ], + }, +}; + +const EverythingSpaceAll: User = { + username: 'everything_space_all', + fullName: 'everything_space_all', + password: 'everything_space_all-password', + role: { + name: 'everything_space_all_role', + kibana: [ + { + base: ['all'], + spaces: ['everything_space'], + }, + ], + }, +}; + +const EverythingSpaceRead: User = { + username: 'everything_space_read', + fullName: 'everything_space_read', + password: 'everything_space_read-password', + role: { + name: 'everything_space_read_role', + kibana: [ + { + base: ['read'], + spaces: ['everything_space'], + }, + ], + }, +}; + +const NothingSpaceAll: User = { + username: 'nothing_space_all', + fullName: 'nothing_space_all', + password: 'nothing_space_all-password', + role: { + name: 'nothing_space_all_role', + kibana: [ + { + base: ['all'], + spaces: ['nothing_space'], + }, + ], + }, +}; + +const NothingSpaceRead: User = { + username: 'nothing_space_read', + fullName: 'nothing_space_read', + password: 'nothing_space_read-password', + role: { + name: 'nothing_space_read_role', + kibana: [ + { + base: ['read'], + spaces: ['nothing_space'], + }, + ], + }, +}; + +export const Users: User[] = [ + NoKibanaPrivileges, + Superuser, + LegacyAll, + DualPrivilegesAll, + DualPrivilegesRead, + GlobalAll, + GlobalRead, + EverythingSpaceAll, + EverythingSpaceRead, + NothingSpaceAll, + NothingSpaceRead, +]; + +const EverythingSpace: Space = { + id: 'everything_space', + name: 'everything_space', + disabledFeatures: [], +}; + +const NothingSpace: Space = { + id: 'nothing_space', + name: 'nothing_space', + disabledFeatures: '*', +}; + +export const Spaces: Space[] = [EverythingSpace, NothingSpace]; + +// For all scenarios, we define both an instance in addition +// to a "type" definition so that we can use the exhaustive switch in +// typescript to ensure all scenarios are handled. + +interface Scenario { + user: User; + space: Space; +} + +interface NoKibanaPrivilegesAtEverythingSpace extends Scenario { + id: 'no_kibana_privileges at everything_space'; +} +const NoKibanaPrivilegesAtEverythingSpace: NoKibanaPrivilegesAtEverythingSpace = { + id: 'no_kibana_privileges at everything_space', + user: NoKibanaPrivileges, + space: EverythingSpace, +}; + +interface NoKibanaPrivilegesAtNothingSpace extends Scenario { + id: 'no_kibana_privileges at nothing_space'; +} +const NoKibanaPrivilegesAtNothingSpace: NoKibanaPrivilegesAtNothingSpace = { + id: 'no_kibana_privileges at nothing_space', + user: NoKibanaPrivileges, + space: NothingSpace, +}; + +interface SuperuserAtEverythingSpace extends Scenario { + id: 'superuser at everything_space'; +} +const SuperuserAtEverythingSpace: SuperuserAtEverythingSpace = { + id: 'superuser at everything_space', + user: Superuser, + space: EverythingSpace, +}; + +interface SuperuserAtNothingSpace extends Scenario { + id: 'superuser at nothing_space'; +} +const SuperuserAtNothingSpace: SuperuserAtNothingSpace = { + id: 'superuser at nothing_space', + user: Superuser, + space: NothingSpace, +}; + +interface LegacyAllAtEverythingSpace extends Scenario { + id: 'legacy_all at everything_space'; +} +const LegacyAllAtEverythingSpace: LegacyAllAtEverythingSpace = { + id: 'legacy_all at everything_space', + user: LegacyAll, + space: EverythingSpace, +}; + +interface LegacyAllAtNothingSpace extends Scenario { + id: 'legacy_all at nothing_space'; +} +const LegacyAllAtNothingSpace: LegacyAllAtNothingSpace = { + id: 'legacy_all at nothing_space', + user: LegacyAll, + space: NothingSpace, +}; + +interface DualPrivilegesAllAtEverythingSpace extends Scenario { + id: 'dual_privileges_all at everything_space'; +} +const DualPrivilegesAllAtEverythingSpace: DualPrivilegesAllAtEverythingSpace = { + id: 'dual_privileges_all at everything_space', + user: DualPrivilegesAll, + space: EverythingSpace, +}; + +interface DualPrivilegesAllAtNothingSpace extends Scenario { + id: 'dual_privileges_all at nothing_space'; +} +const DualPrivilegesAllAtNothingSpace: DualPrivilegesAllAtNothingSpace = { + id: 'dual_privileges_all at nothing_space', + user: DualPrivilegesAll, + space: NothingSpace, +}; + +interface DualPrivilegesReadAtEverythingSpace extends Scenario { + id: 'dual_privileges_read at everything_space'; +} +const DualPrivilegesReadAtEverythingSpace: DualPrivilegesReadAtEverythingSpace = { + id: 'dual_privileges_read at everything_space', + user: DualPrivilegesRead, + space: EverythingSpace, +}; + +interface DualPrivilegesReadAtNothingSpace extends Scenario { + id: 'dual_privileges_read at nothing_space'; +} +const DualPrivilegesReadAtNothingSpace: DualPrivilegesReadAtNothingSpace = { + id: 'dual_privileges_read at nothing_space', + user: DualPrivilegesRead, + space: NothingSpace, +}; + +interface GlobalAllAtEverythingSpace extends Scenario { + id: 'global_all at everything_space'; +} +const GlobalAllAtEverythingSpace: GlobalAllAtEverythingSpace = { + id: 'global_all at everything_space', + user: GlobalAll, + space: EverythingSpace, +}; + +interface GlobalAllAtNothingSpace extends Scenario { + id: 'global_all at nothing_space'; +} +const GlobalAllAtNothingSpace: GlobalAllAtNothingSpace = { + id: 'global_all at nothing_space', + user: GlobalAll, + space: NothingSpace, +}; + +interface GlobalReadAtEverythingSpace extends Scenario { + id: 'global_read at everything_space'; +} +const GlobalReadAtEverythingSpace: GlobalReadAtEverythingSpace = { + id: 'global_read at everything_space', + user: GlobalRead, + space: EverythingSpace, +}; + +interface GlobalReadAtNothingSpace extends Scenario { + id: 'global_read at nothing_space'; +} +const GlobalReadAtNothingSpace: GlobalReadAtNothingSpace = { + id: 'global_read at nothing_space', + user: GlobalRead, + space: NothingSpace, +}; + +interface EverythingSpaceAllAtEverythingSpace extends Scenario { + id: 'everything_space_all at everything_space'; +} +const EverythingSpaceAllAtEverythingSpace: EverythingSpaceAllAtEverythingSpace = { + id: 'everything_space_all at everything_space', + user: EverythingSpaceAll, + space: EverythingSpace, +}; + +interface EverythingSpaceAllAtNothingSpace extends Scenario { + id: 'everything_space_all at nothing_space'; +} +const EverythingSpaceAllAtNothingSpace: EverythingSpaceAllAtNothingSpace = { + id: 'everything_space_all at nothing_space', + user: EverythingSpaceAll, + space: NothingSpace, +}; + +interface EverythingSpaceReadAtEverythingSpace extends Scenario { + id: 'everything_space_read at everything_space'; +} +const EverythingSpaceReadAtEverythingSpace: EverythingSpaceReadAtEverythingSpace = { + id: 'everything_space_read at everything_space', + user: EverythingSpaceRead, + space: EverythingSpace, +}; + +interface EverythingSpaceReadAtNothingSpace extends Scenario { + id: 'everything_space_read at nothing_space'; +} +const EverythingSpaceReadAtNothingSpace: EverythingSpaceReadAtNothingSpace = { + id: 'everything_space_read at nothing_space', + user: EverythingSpaceRead, + space: NothingSpace, +}; + +interface NothingSpaceAllAtEverythingSpace extends Scenario { + id: 'nothing_space_all at everything_space'; +} +const NothingSpaceAllAtEverythingSpace: NothingSpaceAllAtEverythingSpace = { + id: 'nothing_space_all at everything_space', + user: NothingSpaceAll, + space: EverythingSpace, +}; + +interface NothingSpaceAllAtNothingSpace extends Scenario { + id: 'nothing_space_all at nothing_space'; +} +const NothingSpaceAllAtNothingSpace: NothingSpaceAllAtNothingSpace = { + id: 'nothing_space_all at nothing_space', + user: NothingSpaceAll, + space: NothingSpace, +}; + +interface NothingSpaceReadAtEverythingSpace extends Scenario { + id: 'nothing_space_read at everything_space'; +} +const NothingSpaceReadAtEverythingSpace: NothingSpaceReadAtEverythingSpace = { + id: 'nothing_space_read at everything_space', + user: NothingSpaceRead, + space: EverythingSpace, +}; + +interface NothingSpaceReadAtNothingSpace extends Scenario { + id: 'nothing_space_read at nothing_space'; +} +const NothingSpaceReadAtNothingSpace: NothingSpaceReadAtNothingSpace = { + id: 'nothing_space_read at nothing_space', + user: NothingSpaceRead, + space: NothingSpace, +}; + +export const UserAtSpaceScenarios: [ + NoKibanaPrivilegesAtEverythingSpace, + NoKibanaPrivilegesAtNothingSpace, + SuperuserAtEverythingSpace, + SuperuserAtNothingSpace, + LegacyAllAtEverythingSpace, + LegacyAllAtNothingSpace, + DualPrivilegesAllAtEverythingSpace, + DualPrivilegesAllAtNothingSpace, + DualPrivilegesReadAtEverythingSpace, + DualPrivilegesReadAtNothingSpace, + GlobalAllAtEverythingSpace, + GlobalAllAtNothingSpace, + GlobalReadAtEverythingSpace, + GlobalReadAtNothingSpace, + EverythingSpaceAllAtEverythingSpace, + EverythingSpaceAllAtNothingSpace, + EverythingSpaceReadAtEverythingSpace, + EverythingSpaceReadAtNothingSpace, + NothingSpaceAllAtEverythingSpace, + NothingSpaceAllAtNothingSpace, + NothingSpaceReadAtEverythingSpace, + NothingSpaceReadAtNothingSpace +] = [ + NoKibanaPrivilegesAtEverythingSpace, + NoKibanaPrivilegesAtNothingSpace, + SuperuserAtEverythingSpace, + SuperuserAtNothingSpace, + LegacyAllAtEverythingSpace, + LegacyAllAtNothingSpace, + DualPrivilegesAllAtEverythingSpace, + DualPrivilegesAllAtNothingSpace, + DualPrivilegesReadAtEverythingSpace, + DualPrivilegesReadAtNothingSpace, + GlobalAllAtEverythingSpace, + GlobalAllAtNothingSpace, + GlobalReadAtEverythingSpace, + GlobalReadAtNothingSpace, + EverythingSpaceAllAtEverythingSpace, + EverythingSpaceAllAtNothingSpace, + EverythingSpaceReadAtEverythingSpace, + EverythingSpaceReadAtNothingSpace, + NothingSpaceAllAtEverythingSpace, + NothingSpaceAllAtNothingSpace, + NothingSpaceReadAtEverythingSpace, + NothingSpaceReadAtNothingSpace, +]; diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts new file mode 100644 index 000000000000..6972ede00abe --- /dev/null +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts @@ -0,0 +1,92 @@ +/* + * 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 { mapValues } from 'lodash'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; +import { + GetUICapabilitiesFailureReason, + UICapabilitiesService, +} from '../../common/services/ui_capabilities'; +import { UserAtSpaceScenarios } from '../scenarios'; + +// eslint-disable-next-line import/no-default-export +export default function catalogueTests({ getService }: KibanaFunctionalTestDefaultProviders) { + const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities'); + + describe('catalogue', () => { + UserAtSpaceScenarios.forEach(scenario => { + it(`${scenario.id}`, async () => { + const { user, space } = scenario; + + const uiCapabilities = await uiCapabilitiesService.get( + { username: user.username, password: user.password }, + space.id + ); + switch (scenario.id) { + case 'superuser at everything_space': { + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('catalogue'); + // everything is enabled + const expected = mapValues(uiCapabilities.value!.catalogue, () => true); + expect(uiCapabilities.value!.catalogue).to.eql(expected); + break; + } + case 'global_all at everything_space': + case 'dual_privileges_all at everything_space': + case 'everything_space_all at everything_space': + case 'global_read at everything_space': + case 'dual_privileges_read at everything_space': + case 'everything_space_read at everything_space': { + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('catalogue'); + // everything except ml and monitoring is enabled + const expected = mapValues( + uiCapabilities.value!.catalogue, + (enabled, catalogueId) => catalogueId !== 'ml' && catalogueId !== 'monitoring' + ); + expect(uiCapabilities.value!.catalogue).to.eql(expected); + break; + } + // the nothing_space has no features enabled, so even if we have + // privileges to perform these actions, we won't be able to + case 'superuser at nothing_space': + case 'global_all at nothing_space': + case 'global_read at nothing_space': + case 'dual_privileges_all at nothing_space': + case 'dual_privileges_read at nothing_space': + case 'nothing_space_all at nothing_space': + case 'nothing_space_read at nothing_space': { + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('catalogue'); + // everything is disabled + const expected = mapValues(uiCapabilities.value!.catalogue, () => false); + expect(uiCapabilities.value!.catalogue).to.eql(expected); + break; + } + // if we don't have access at the space itself, we're + // redirected to the space selector and the ui capabilities + // are lagely irrelevant because they won't be consumed + case 'no_kibana_privileges at everything_space': + case 'no_kibana_privileges at nothing_space': + case 'legacy_all at everything_space': + case 'legacy_all at nothing_space': + case 'everything_space_all at nothing_space': + case 'everything_space_read at nothing_space': + case 'nothing_space_all at everything_space': + case 'nothing_space_read at everything_space': + expect(uiCapabilities.success).to.be(false); + expect(uiCapabilities.failureReason).to.be( + GetUICapabilitiesFailureReason.RedirectedToRoot + ); + break; + default: + throw new UnreachableError(scenario); + } + }); + }); + }); +} diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/foo.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/foo.ts new file mode 100644 index 000000000000..cf0731769ac2 --- /dev/null +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/foo.ts @@ -0,0 +1,96 @@ +/* + * 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 { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; +import { + GetUICapabilitiesFailureReason, + UICapabilitiesService, +} from '../../common/services/ui_capabilities'; +import { UserAtSpaceScenarios } from '../scenarios'; + +// eslint-disable-next-line import/no-default-export +export default function fooTests({ getService }: KibanaFunctionalTestDefaultProviders) { + const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities'); + + describe('foo', () => { + UserAtSpaceScenarios.forEach(scenario => { + it(`${scenario.id}`, async () => { + const { user, space } = scenario; + + const uiCapabilities = await uiCapabilitiesService.get( + { username: user.username, password: user.password }, + space.id + ); + switch (scenario.id) { + // these users have a read/write view + case 'superuser at everything_space': + case 'global_all at everything_space': + case 'dual_privileges_all at everything_space': + case 'everything_space_all at everything_space': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('foo'); + expect(uiCapabilities.value!.foo).to.eql({ + create: true, + edit: true, + delete: true, + show: true, + }); + break; + // these users have a read only view + case 'global_read at everything_space': + case 'dual_privileges_read at everything_space': + case 'everything_space_read at everything_space': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('foo'); + expect(uiCapabilities.value!.foo).to.eql({ + create: false, + edit: false, + delete: false, + show: true, + }); + break; + // the nothing_space has no features enabled, so even if we have + // privileges to perform these actions, we won't be able to + case 'superuser at nothing_space': + case 'global_all at nothing_space': + case 'global_read at nothing_space': + case 'dual_privileges_all at nothing_space': + case 'dual_privileges_read at nothing_space': + case 'nothing_space_all at nothing_space': + case 'nothing_space_read at nothing_space': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('foo'); + expect(uiCapabilities.value!.foo).to.eql({ + create: false, + edit: false, + delete: false, + show: false, + }); + break; + // if we don't have access at the space itself, we're + // redirected to the space selector and the ui capabilities + // are largely irrelevant because they won't be consumed + case 'no_kibana_privileges at everything_space': + case 'no_kibana_privileges at nothing_space': + case 'legacy_all at everything_space': + case 'legacy_all at nothing_space': + case 'everything_space_all at nothing_space': + case 'everything_space_read at nothing_space': + case 'nothing_space_all at everything_space': + case 'nothing_space_read at everything_space': + expect(uiCapabilities.success).to.be(false); + expect(uiCapabilities.failureReason).to.be( + GetUICapabilitiesFailureReason.RedirectedToRoot + ); + break; + default: + throw new UnreachableError(scenario); + } + }); + }); + }); +} diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/index.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/index.ts new file mode 100644 index 000000000000..840fbecb18d9 --- /dev/null +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/index.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 { SpacesService } from '../../../common/services'; +import { SecurityService } from '../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; +import { FeaturesService } from '../../common/services'; +import { isCustomRoleSpecification } from '../../common/types'; +import { Spaces, Users } from '../scenarios'; + +// eslint-disable-next-line import/no-default-export +export default function uiCapabilitiesTests({ + loadTestFile, + getService, +}: KibanaFunctionalTestDefaultProviders) { + const securityService: SecurityService = getService('security'); + const spacesService: SpacesService = getService('spaces'); + const featuresService: FeaturesService = getService('features'); + + describe('ui capabilities', function() { + this.tags('ciGroup5'); + + before(async () => { + const features = await featuresService.get(); + for (const space of Spaces) { + const disabledFeatures = + space.disabledFeatures === '*' ? Object.keys(features) : space.disabledFeatures; + await spacesService.create({ + ...space, + disabledFeatures, + }); + } + + for (const user of Users) { + const roles = [...(user.role ? [user.role] : []), ...(user.roles ? user.roles : [])]; + + await securityService.user.create(user.username, { + password: user.password, + full_name: user.fullName, + roles: roles.map(role => role.name), + }); + + for (const role of roles) { + if (isCustomRoleSpecification(role)) { + await securityService.role.create(role.name, { + kibana: role.kibana, + }); + } + } + } + }); + + after(async () => { + for (const space of Spaces) { + await spacesService.delete(space.id); + } + + for (const user of Users) { + await securityService.user.delete(user.username); + + const roles = [...(user.role ? [user.role] : []), ...(user.roles ? user.roles : [])]; + for (const role of roles) { + if (isCustomRoleSpecification(role)) { + await securityService.role.delete(role.name); + } + } + } + }); + + loadTestFile(require.resolve('./catalogue')); + loadTestFile(require.resolve('./foo')); + loadTestFile(require.resolve('./nav_links')); + loadTestFile(require.resolve('./saved_objects_management')); + }); +} diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts new file mode 100644 index 000000000000..7bbf39f11847 --- /dev/null +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts @@ -0,0 +1,85 @@ +/* + * 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 { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; +import { NavLinksBuilder } from '../../common/nav_links_builder'; +import { FeaturesService } from '../../common/services'; +import { + GetUICapabilitiesFailureReason, + UICapabilitiesService, +} from '../../common/services/ui_capabilities'; +import { UserAtSpaceScenarios } from '../scenarios'; + +// eslint-disable-next-line import/no-default-export +export default function navLinksTests({ getService }: KibanaFunctionalTestDefaultProviders) { + const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities'); + const featuresService: FeaturesService = getService('features'); + + describe('navLinks', () => { + let navLinksBuilder: NavLinksBuilder; + before(async () => { + const features = await featuresService.get(); + navLinksBuilder = new NavLinksBuilder(features); + }); + + UserAtSpaceScenarios.forEach(scenario => { + it(`${scenario.id}`, async () => { + const { user, space } = scenario; + + const uiCapabilities = await uiCapabilitiesService.get( + { username: user.username, password: user.password }, + space.id + ); + switch (scenario.id) { + case 'superuser at everything_space': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('navLinks'); + expect(uiCapabilities.value!.navLinks).to.eql(navLinksBuilder.all()); + break; + case 'global_all at everything_space': + case 'dual_privileges_all at everything_space': + case 'dual_privileges_read at everything_space': + case 'global_read at everything_space': + case 'everything_space_all at everything_space': + case 'everything_space_read at everything_space': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('navLinks'); + expect(uiCapabilities.value!.navLinks).to.eql( + navLinksBuilder.except('ml', 'monitoring') + ); + break; + case 'superuser at nothing_space': + case 'global_all at nothing_space': + case 'dual_privileges_all at nothing_space': + case 'dual_privileges_read at nothing_space': + case 'global_read at nothing_space': + case 'nothing_space_all at nothing_space': + case 'nothing_space_read at nothing_space': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('navLinks'); + expect(uiCapabilities.value!.navLinks).to.eql(navLinksBuilder.only('management')); + break; + case 'no_kibana_privileges at everything_space': + case 'no_kibana_privileges at nothing_space': + case 'legacy_all at everything_space': + case 'legacy_all at nothing_space': + case 'everything_space_all at nothing_space': + case 'everything_space_read at nothing_space': + case 'nothing_space_all at everything_space': + case 'nothing_space_read at everything_space': + expect(uiCapabilities.success).to.be(false); + expect(uiCapabilities.failureReason).to.be( + GetUICapabilitiesFailureReason.RedirectedToRoot + ); + break; + default: + throw new UnreachableError(scenario); + } + }); + }); + }); +} diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/saved_objects_management.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/saved_objects_management.ts new file mode 100644 index 000000000000..e2380e592adb --- /dev/null +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/saved_objects_management.ts @@ -0,0 +1,114 @@ +/* + * 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 { mapValues } from 'lodash'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; +import { SavedObjectsManagementBuilder } from '../../common/saved_objects_management_builder'; +import { + GetUICapabilitiesFailureReason, + UICapabilitiesService, +} from '../../common/services/ui_capabilities'; +import { UserAtSpaceScenarios } from '../scenarios'; + +const savedObjectsManagementBuilder = new SavedObjectsManagementBuilder(true); + +// eslint-disable-next-line import/no-default-export +export default function savedObjectsManagementTests({ + getService, +}: KibanaFunctionalTestDefaultProviders) { + const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities'); + + describe('savedObjectsManagement', () => { + UserAtSpaceScenarios.forEach(scenario => { + it(`${scenario.id}`, async () => { + const { user, space } = scenario; + + const uiCapabilities = await uiCapabilitiesService.get( + { username: user.username, password: user.password }, + space.id + ); + switch (scenario.id) { + case 'superuser at everything_space': + case 'superuser at nothing_space': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('savedObjectsManagement'); + const expected = mapValues(uiCapabilities.value!.savedObjectsManagement, () => + savedObjectsManagementBuilder.uiCapabilities('all') + ); + expect(uiCapabilities.value!.savedObjectsManagement).to.eql(expected); + break; + case 'global_all at everything_space': + case 'dual_privileges_all at everything_space': + case 'everything_space_all at everything_space': + case 'global_all at nothing_space': + case 'dual_privileges_all at nothing_space': + case 'nothing_space_all at nothing_space': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('savedObjectsManagement'); + expect(uiCapabilities.value!.savedObjectsManagement).to.eql( + savedObjectsManagementBuilder.build({ + all: [ + 'config', + 'graph-workspace', + 'map', + 'canvas-workpad', + 'index-pattern', + 'visualization', + 'search', + 'dashboard', + 'timelion-sheet', + 'url', + 'infrastructure-ui-source', + ], + }) + ); + break; + case 'dual_privileges_read at everything_space': + case 'global_read at everything_space': + case 'everything_space_read at everything_space': + case 'dual_privileges_read at nothing_space': + case 'global_read at nothing_space': + case 'nothing_space_read at nothing_space': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('savedObjectsManagement'); + expect(uiCapabilities.value!.savedObjectsManagement).to.eql( + savedObjectsManagementBuilder.build({ + read: [ + 'config', + 'graph-workspace', + 'map', + 'canvas-workpad', + 'index-pattern', + 'visualization', + 'search', + 'dashboard', + 'timelion-sheet', + 'url', + 'infrastructure-ui-source', + ], + }) + ); + break; + case 'no_kibana_privileges at everything_space': + case 'no_kibana_privileges at nothing_space': + case 'legacy_all at everything_space': + case 'legacy_all at nothing_space': + case 'everything_space_all at nothing_space': + case 'everything_space_read at nothing_space': + case 'nothing_space_all at everything_space': + case 'nothing_space_read at everything_space': + expect(uiCapabilities.success).to.be(false); + expect(uiCapabilities.failureReason).to.be( + GetUICapabilitiesFailureReason.RedirectedToRoot + ); + break; + default: + throw new UnreachableError(scenario); + } + }); + }); + }); +} diff --git a/x-pack/test/ui_capabilities/security_only/config.ts b/x-pack/test/ui_capabilities/security_only/config.ts new file mode 100644 index 000000000000..6b32ccc1b70d --- /dev/null +++ b/x-pack/test/ui_capabilities/security_only/config.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. + */ + +import { createTestConfig } from '../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('security_only', { disabledPlugins: ['spaces'], license: 'trial' }); diff --git a/x-pack/test/ui_capabilities/security_only/scenarios.ts b/x-pack/test/ui_capabilities/security_only/scenarios.ts new file mode 100644 index 000000000000..8f7e483417b5 --- /dev/null +++ b/x-pack/test/ui_capabilities/security_only/scenarios.ts @@ -0,0 +1,215 @@ +/* + * 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 { CustomRoleSpecification, User } from '../common/types'; + +// For all scenarios, we define both an instance in addition +// to a "type" definition so that we can use the exhaustive switch in +// typescript to ensure all scenarios are handled. + +const allRole: CustomRoleSpecification = { + name: 'all_role', + kibana: [ + { + base: ['all'], + spaces: ['*'], + }, + ], +}; + +interface NoKibanaPrivileges extends User { + username: 'no_kibana_privileges'; +} +const NoKibanaPrivileges: NoKibanaPrivileges = { + username: 'no_kibana_privileges', + fullName: 'no_kibana_privileges', + password: 'no_kibana_privileges-password', + role: { + name: 'no_kibana_privileges', + elasticsearch: { + indices: [ + { + names: ['foo'], + privileges: ['all'], + }, + ], + }, + }, +}; + +interface Superuser extends User { + username: 'superuser'; +} +const Superuser: Superuser = { + username: 'superuser', + fullName: 'superuser', + password: 'superuser-password', + role: { + name: 'superuser', + }, +}; + +interface LegacyAll extends User { + username: 'legacy_all'; +} +const LegacyAll: LegacyAll = { + username: 'legacy_all', + fullName: 'legacy_all', + password: 'legacy_all-password', + role: { + name: 'legacy_all_role', + elasticsearch: { + indices: [ + { + names: ['.kibana*'], + privileges: ['all'], + }, + ], + }, + }, +}; + +interface DualPrivilegesAll extends User { + username: 'dual_privileges_all'; +} +const DualPrivilegesAll: DualPrivilegesAll = { + username: 'dual_privileges_all', + fullName: 'dual_privileges_all', + password: 'dual_privileges_all-password', + role: { + name: 'dual_privileges_all_role', + elasticsearch: { + indices: [ + { + names: ['.kibana*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + base: ['all'], + spaces: ['*'], + }, + ], + }, +}; + +interface DualPrivilegesRead extends User { + username: 'dual_privileges_read'; +} +const DualPrivilegesRead: DualPrivilegesRead = { + username: 'dual_privileges_read', + fullName: 'dual_privileges_read', + password: 'dual_privileges_read-password', + role: { + name: 'dual_privileges_read_role', + elasticsearch: { + indices: [ + { + names: ['.kibana*'], + privileges: ['read'], + }, + ], + }, + kibana: [ + { + base: ['read'], + spaces: ['*'], + }, + ], + }, +}; + +interface All extends User { + username: 'all'; +} +const All: All = { + username: 'all', + fullName: 'all', + password: 'all-password', + role: allRole, +}; + +interface Read extends User { + username: 'read'; +} +const Read: Read = { + username: 'read', + fullName: 'read', + password: 'read-password', + role: { + name: 'read_role', + kibana: [ + { + base: ['read'], + spaces: ['*'], + }, + ], + }, +}; + +interface FooAll extends User { + username: 'foo_all'; +} +const FooAll: FooAll = { + username: 'foo_all', + fullName: 'foo_all', + password: 'foo_all-password', + role: { + name: 'foo_all_role', + kibana: [ + { + feature: { + foo: ['all'], + }, + spaces: ['*'], + }, + ], + }, +}; + +interface FooRead extends User { + username: 'foo_read'; +} +const FooRead: FooRead = { + username: 'foo_read', + fullName: 'foo_read', + password: 'foo_read-password', + role: { + name: 'foo_read_role', + kibana: [ + { + feature: { + foo: ['read'], + }, + spaces: ['*'], + }, + ], + }, +}; + +export const UserScenarios: [ + NoKibanaPrivileges, + Superuser, + LegacyAll, + DualPrivilegesAll, + DualPrivilegesRead, + All, + Read, + FooAll, + FooRead +] = [ + NoKibanaPrivileges, + Superuser, + LegacyAll, + DualPrivilegesAll, + DualPrivilegesRead, + All, + Read, + FooAll, + FooRead, +]; diff --git a/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts b/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts new file mode 100644 index 000000000000..fe4eca19fe8e --- /dev/null +++ b/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { mapValues } from 'lodash'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; +import { + GetUICapabilitiesFailureReason, + UICapabilitiesService, +} from '../../common/services/ui_capabilities'; +import { UserScenarios } from '../scenarios'; + +// eslint-disable-next-line import/no-default-export +export default function catalogueTests({ getService }: KibanaFunctionalTestDefaultProviders) { + const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities'); + + describe('catalogue', () => { + UserScenarios.forEach(scenario => { + it(`${scenario.fullName}`, async () => { + const uiCapabilities = await uiCapabilitiesService.get({ + username: scenario.username, + password: scenario.password, + }); + switch (scenario.username) { + case 'superuser': { + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('catalogue'); + // everything is enabled + const expected = mapValues(uiCapabilities.value!.catalogue, () => true); + expect(uiCapabilities.value!.catalogue).to.eql(expected); + break; + } + case 'all': + case 'read': + case 'dual_privileges_all': + case 'dual_privileges_read': { + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('catalogue'); + // everything except ml and monitoring is enabled + const expected = mapValues( + uiCapabilities.value!.catalogue, + (enabled, catalogueId) => catalogueId !== 'ml' && catalogueId !== 'monitoring' + ); + expect(uiCapabilities.value!.catalogue).to.eql(expected); + break; + } + case 'foo_all': + case 'foo_read': { + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('catalogue'); + // only foo is enabled + const expected = mapValues( + uiCapabilities.value!.catalogue, + (value, catalogueId) => catalogueId === 'foo' + ); + expect(uiCapabilities.value!.catalogue).to.eql(expected); + break; + } + // these users have no access to even get the ui capabilities + case 'legacy_all': + case 'no_kibana_privileges': + expect(uiCapabilities.success).to.be(false); + expect(uiCapabilities.failureReason).to.be(GetUICapabilitiesFailureReason.NotFound); + break; + default: + throw new UnreachableError(scenario); + } + }); + }); + }); +} diff --git a/x-pack/test/ui_capabilities/security_only/tests/foo.ts b/x-pack/test/ui_capabilities/security_only/tests/foo.ts new file mode 100644 index 000000000000..ab6090d17bb1 --- /dev/null +++ b/x-pack/test/ui_capabilities/security_only/tests/foo.ts @@ -0,0 +1,67 @@ +/* + * 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 { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; +import { + GetUICapabilitiesFailureReason, + UICapabilitiesService, +} from '../../common/services/ui_capabilities'; +import { UserScenarios } from '../scenarios'; + +// eslint-disable-next-line import/no-default-export +export default function fooTests({ getService }: KibanaFunctionalTestDefaultProviders) { + const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities'); + + describe('foo', () => { + UserScenarios.forEach(scenario => { + it(`${scenario.fullName}`, async () => { + const uiCapabilities = await uiCapabilitiesService.get({ + username: scenario.username, + password: scenario.password, + }); + switch (scenario.username) { + // these users have a read/write view of Foo + case 'superuser': + case 'all': + case 'dual_privileges_all': + case 'foo_all': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('foo'); + expect(uiCapabilities.value!.foo).to.eql({ + create: true, + edit: true, + delete: true, + show: true, + }); + break; + // these users have a read-only view of Foo + case 'read': + case 'dual_privileges_read': + case 'foo_read': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('foo'); + expect(uiCapabilities.value!.foo).to.eql({ + create: false, + edit: false, + delete: false, + show: true, + }); + break; + // these users have no access to even get the ui capabilities + case 'legacy_all': + case 'no_kibana_privileges': + expect(uiCapabilities.success).to.be(false); + expect(uiCapabilities.failureReason).to.be(GetUICapabilitiesFailureReason.NotFound); + break; + // all other users can't do anything with Foo + default: + throw new UnreachableError(scenario); + } + }); + }); + }); +} diff --git a/x-pack/test/ui_capabilities/security_only/tests/index.ts b/x-pack/test/ui_capabilities/security_only/tests/index.ts new file mode 100644 index 000000000000..e69cef8b3136 --- /dev/null +++ b/x-pack/test/ui_capabilities/security_only/tests/index.ts @@ -0,0 +1,60 @@ +/* + * 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 { SecurityService } from '../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; +import { isCustomRoleSpecification } from '../../common/types'; +import { UserScenarios } from '../scenarios'; + +// eslint-disable-next-line import/no-default-export +export default function uiCapabilitesTests({ + loadTestFile, + getService, +}: KibanaFunctionalTestDefaultProviders) { + const securityService: SecurityService = getService('security'); + + describe('ui capabilities', function() { + this.tags('ciGroup5'); + + before(async () => { + for (const user of UserScenarios) { + const roles = [...(user.role ? [user.role] : []), ...(user.roles ? user.roles : [])]; + + await securityService.user.create(user.username, { + password: user.password, + full_name: user.fullName, + roles: roles.map(role => role.name), + }); + + for (const role of roles) { + if (isCustomRoleSpecification(role)) { + await securityService.role.create(role.name, { + kibana: role.kibana, + }); + } + } + } + }); + + after(async () => { + for (const user of UserScenarios) { + await securityService.user.delete(user.username); + + const roles = [...(user.role ? [user.role] : []), ...(user.roles ? user.roles : [])]; + for (const role of roles) { + if (isCustomRoleSpecification(role)) { + await securityService.role.delete(role.name); + } + } + } + }); + + loadTestFile(require.resolve('./catalogue')); + loadTestFile(require.resolve('./foo')); + loadTestFile(require.resolve('./nav_links')); + loadTestFile(require.resolve('./saved_objects_management')); + }); +} diff --git a/x-pack/test/ui_capabilities/security_only/tests/nav_links.ts b/x-pack/test/ui_capabilities/security_only/tests/nav_links.ts new file mode 100644 index 000000000000..4b8cc06bd568 --- /dev/null +++ b/x-pack/test/ui_capabilities/security_only/tests/nav_links.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; +import { NavLinksBuilder } from '../../common/nav_links_builder'; +import { FeaturesService } from '../../common/services'; +import { + GetUICapabilitiesFailureReason, + UICapabilitiesService, +} from '../../common/services/ui_capabilities'; +import { UserScenarios } from '../scenarios'; + +// eslint-disable-next-line import/no-default-export +export default function navLinksTests({ getService }: KibanaFunctionalTestDefaultProviders) { + const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities'); + const featuresService: FeaturesService = getService('features'); + + describe('navLinks', () => { + let navLinksBuilder: NavLinksBuilder; + before(async () => { + const features = await featuresService.get(); + navLinksBuilder = new NavLinksBuilder(features); + }); + + UserScenarios.forEach(scenario => { + it(`${scenario.fullName}`, async () => { + const uiCapabilities = await uiCapabilitiesService.get({ + username: scenario.username, + password: scenario.password, + }); + switch (scenario.username) { + case 'superuser': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('navLinks'); + expect(uiCapabilities.value!.navLinks).to.eql(navLinksBuilder.all()); + break; + case 'all': + case 'read': + case 'dual_privileges_all': + case 'dual_privileges_read': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('navLinks'); + expect(uiCapabilities.value!.navLinks).to.eql( + navLinksBuilder.except('ml', 'monitoring') + ); + break; + case 'foo_all': + case 'foo_read': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('navLinks'); + expect(uiCapabilities.value!.navLinks).to.eql( + navLinksBuilder.only('management', 'foo') + ); + break; + case 'legacy_all': + case 'no_kibana_privileges': + expect(uiCapabilities.success).to.be(false); + expect(uiCapabilities.failureReason).to.be(GetUICapabilitiesFailureReason.NotFound); + break; + default: + throw new UnreachableError(scenario); + } + }); + }); + }); +} diff --git a/x-pack/test/ui_capabilities/security_only/tests/saved_objects_management.ts b/x-pack/test/ui_capabilities/security_only/tests/saved_objects_management.ts new file mode 100644 index 000000000000..403c1b6b904d --- /dev/null +++ b/x-pack/test/ui_capabilities/security_only/tests/saved_objects_management.ts @@ -0,0 +1,115 @@ +/* + * 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 { mapValues } from 'lodash'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; +import { SavedObjectsManagementBuilder } from '../../common/saved_objects_management_builder'; +import { + GetUICapabilitiesFailureReason, + UICapabilitiesService, +} from '../../common/services/ui_capabilities'; +import { UserScenarios } from '../scenarios'; + +const savedObjectsManagementBuilder = new SavedObjectsManagementBuilder(false); + +// eslint-disable-next-line import/no-default-export +export default function savedObjectsManagementTests({ + getService, +}: KibanaFunctionalTestDefaultProviders) { + const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities'); + + describe('savedObjectsManagement', () => { + UserScenarios.forEach(scenario => { + it(`${scenario.fullName}`, async () => { + const uiCapabilities = await uiCapabilitiesService.get({ + username: scenario.username, + password: scenario.password, + }); + switch (scenario.username) { + case 'superuser': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('savedObjectsManagement'); + const expected = mapValues(uiCapabilities.value!.savedObjectsManagement, () => + savedObjectsManagementBuilder.uiCapabilities('all') + ); + expect(uiCapabilities.value!.savedObjectsManagement).to.eql(expected); + break; + case 'all': + case 'dual_privileges_all': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('savedObjectsManagement'); + expect(uiCapabilities.value!.savedObjectsManagement).to.eql( + savedObjectsManagementBuilder.build({ + all: [ + 'config', + 'graph-workspace', + 'map', + 'canvas-workpad', + 'index-pattern', + 'visualization', + 'search', + 'dashboard', + 'timelion-sheet', + 'url', + 'infrastructure-ui-source', + ], + }) + ); + break; + case 'read': + case 'dual_privileges_read': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('savedObjectsManagement'); + expect(uiCapabilities.value!.savedObjectsManagement).to.eql( + savedObjectsManagementBuilder.build({ + read: [ + 'config', + 'graph-workspace', + 'map', + 'canvas-workpad', + 'index-pattern', + 'visualization', + 'search', + 'dashboard', + 'timelion-sheet', + 'url', + 'infrastructure-ui-source', + ], + }) + ); + break; + case 'foo_all': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('savedObjectsManagement'); + expect(uiCapabilities.value!.savedObjectsManagement).to.eql( + savedObjectsManagementBuilder.build({ + all: ['foo'], + read: ['index-pattern', 'config'], + }) + ); + break; + case 'foo_read': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('savedObjectsManagement'); + expect(uiCapabilities.value!.savedObjectsManagement).to.eql( + savedObjectsManagementBuilder.build({ + read: ['foo', 'index-pattern', 'config'], + }) + ); + break; + case 'no_kibana_privileges': + case 'legacy_all': + expect(uiCapabilities.success).to.be(false); + expect(uiCapabilities.failureReason).to.be(GetUICapabilitiesFailureReason.NotFound); + break; + default: + throw new UnreachableError(scenario); + } + }); + }); + }); +} diff --git a/x-pack/test/ui_capabilities/spaces_only/config.ts b/x-pack/test/ui_capabilities/spaces_only/config.ts new file mode 100644 index 000000000000..12cfa1c282c8 --- /dev/null +++ b/x-pack/test/ui_capabilities/spaces_only/config.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. + */ + +import { createTestConfig } from '../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('spaces_only', { license: 'basic' }); diff --git a/x-pack/test/ui_capabilities/spaces_only/scenarios.ts b/x-pack/test/ui_capabilities/spaces_only/scenarios.ts new file mode 100644 index 000000000000..c5f31c1ca22c --- /dev/null +++ b/x-pack/test/ui_capabilities/spaces_only/scenarios.ts @@ -0,0 +1,44 @@ +/* + * 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 { Space } from '../common/types'; + +// For all scenarios, we define both an instance in addition +// to a "type" definition so that we can use the exhaustive switch in +// typescript to ensure all scenarios are handled. + +interface EverythingSpace extends Space { + id: 'everything_space'; +} +const EverythingSpace: EverythingSpace = { + id: 'everything_space', + name: 'everything_space', + disabledFeatures: [], +}; + +interface NothingSpace extends Space { + id: 'nothing_space'; +} +const NothingSpace: NothingSpace = { + id: 'nothing_space', + name: 'nothing_space', + disabledFeatures: '*', +}; + +interface FooDisabledSpace extends Space { + id: 'foo_disabled_space'; +} +const FooDisabledSpace: FooDisabledSpace = { + id: 'foo_disabled_space', + name: 'foo_disabled_space', + disabledFeatures: ['foo'], +}; + +export const SpaceScenarios: [EverythingSpace, NothingSpace, FooDisabledSpace] = [ + EverythingSpace, + NothingSpace, + FooDisabledSpace, +]; diff --git a/x-pack/test/ui_capabilities/spaces_only/tests/catalogue.ts b/x-pack/test/ui_capabilities/spaces_only/tests/catalogue.ts new file mode 100644 index 000000000000..f0aac4183a08 --- /dev/null +++ b/x-pack/test/ui_capabilities/spaces_only/tests/catalogue.ts @@ -0,0 +1,55 @@ +/* + * 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 { mapValues } from 'lodash'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; +import { UICapabilitiesService } from '../../common/services/ui_capabilities'; +import { SpaceScenarios } from '../scenarios'; + +// eslint-disable-next-line import/no-default-export +export default function catalogueTests({ getService }: KibanaFunctionalTestDefaultProviders) { + const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities'); + + describe('catalogue', () => { + SpaceScenarios.forEach(scenario => { + it(`${scenario.name}`, async () => { + const uiCapabilities = await uiCapabilitiesService.get(null, scenario.id); + switch (scenario.id) { + case 'everything_space': { + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('catalogue'); + // everything is enabled + const expected = mapValues(uiCapabilities.value!.catalogue, () => true); + expect(uiCapabilities.value!.catalogue).to.eql(expected); + break; + } + case 'nothing_space': { + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('catalogue'); + // everything is disabled + const expected = mapValues(uiCapabilities.value!.catalogue, () => false); + expect(uiCapabilities.value!.catalogue).to.eql(expected); + break; + } + case 'foo_disabled_space': { + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('catalogue'); + // only foo is disabled + const expected = mapValues( + uiCapabilities.value!.catalogue, + (value, catalogueId) => catalogueId !== 'foo' + ); + expect(uiCapabilities.value!.catalogue).to.eql(expected); + break; + } + default: + throw new UnreachableError(scenario); + } + }); + }); + }); +} diff --git a/x-pack/test/ui_capabilities/spaces_only/tests/foo.ts b/x-pack/test/ui_capabilities/spaces_only/tests/foo.ts new file mode 100644 index 000000000000..7840dc719cdc --- /dev/null +++ b/x-pack/test/ui_capabilities/spaces_only/tests/foo.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; +import { UICapabilitiesService } from '../../common/services/ui_capabilities'; +import { SpaceScenarios } from '../scenarios'; + +// eslint-disable-next-line import/no-default-export +export default function fooTests({ getService }: KibanaFunctionalTestDefaultProviders) { + const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities'); + + describe('foo', () => { + SpaceScenarios.forEach(scenario => { + it(`${scenario.name}`, async () => { + const uiCapabilities = await uiCapabilitiesService.get(null, scenario.id); + switch (scenario.id) { + case 'everything_space': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('foo'); + expect(uiCapabilities.value!.foo).to.eql({ + create: true, + edit: true, + delete: true, + show: true, + }); + break; + case 'nothing_space': + case 'foo_disabled_space': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('foo'); + expect(uiCapabilities.value!.foo).to.eql({ + create: false, + edit: false, + delete: false, + show: false, + }); + break; + default: + throw new UnreachableError(scenario); + } + }); + }); + }); +} diff --git a/x-pack/test/ui_capabilities/spaces_only/tests/index.ts b/x-pack/test/ui_capabilities/spaces_only/tests/index.ts new file mode 100644 index 000000000000..0d8164aa584d --- /dev/null +++ b/x-pack/test/ui_capabilities/spaces_only/tests/index.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SpacesService } from '../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; +import { FeaturesService } from '../../common/services'; +import { SpaceScenarios } from '../scenarios'; + +// eslint-disable-next-line import/no-default-export +export default function uiCapabilitesTests({ + loadTestFile, + getService, +}: KibanaFunctionalTestDefaultProviders) { + const spacesService: SpacesService = getService('spaces'); + const featuresService: FeaturesService = getService('features'); + + describe('ui capabilities', function() { + this.tags('ciGroup5'); + + before(async () => { + const features = await featuresService.get(); + for (const space of SpaceScenarios) { + const disabledFeatures = + space.disabledFeatures === '*' ? Object.keys(features) : space.disabledFeatures; + await spacesService.create({ + ...space, + disabledFeatures, + }); + } + }); + + after(async () => { + for (const space of SpaceScenarios) { + await spacesService.delete(space.id); + } + }); + + loadTestFile(require.resolve('./catalogue')); + loadTestFile(require.resolve('./foo')); + loadTestFile(require.resolve('./nav_links')); + loadTestFile(require.resolve('./saved_objects_management')); + }); +} diff --git a/x-pack/test/ui_capabilities/spaces_only/tests/nav_links.ts b/x-pack/test/ui_capabilities/spaces_only/tests/nav_links.ts new file mode 100644 index 000000000000..03fd1934abba --- /dev/null +++ b/x-pack/test/ui_capabilities/spaces_only/tests/nav_links.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 expect from '@kbn/expect'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; +import { NavLinksBuilder } from '../../common/nav_links_builder'; +import { FeaturesService } from '../../common/services'; +import { UICapabilitiesService } from '../../common/services/ui_capabilities'; +import { SpaceScenarios } from '../scenarios'; + +// eslint-disable-next-line import/no-default-export +export default function navLinksTests({ getService }: KibanaFunctionalTestDefaultProviders) { + const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities'); + const featuresService: FeaturesService = getService('features'); + + describe('navLinks', () => { + let navLinksBuilder: NavLinksBuilder; + before(async () => { + const features = await featuresService.get(); + navLinksBuilder = new NavLinksBuilder(features); + }); + + SpaceScenarios.forEach(scenario => { + it(`${scenario.name}`, async () => { + const uiCapabilities = await uiCapabilitiesService.get(null, scenario.id); + switch (scenario.id) { + case 'everything_space': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('navLinks'); + expect(uiCapabilities.value!.navLinks).to.eql(navLinksBuilder.all()); + break; + case 'nothing_space': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('navLinks'); + expect(uiCapabilities.value!.navLinks).to.eql(navLinksBuilder.only('management')); + break; + case 'foo_disabled_space': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('navLinks'); + expect(uiCapabilities.value!.navLinks).to.eql(navLinksBuilder.except('foo')); + break; + default: + throw new UnreachableError(scenario); + } + }); + }); + }); +} diff --git a/x-pack/test/ui_capabilities/spaces_only/tests/saved_objects_management.ts b/x-pack/test/ui_capabilities/spaces_only/tests/saved_objects_management.ts new file mode 100644 index 000000000000..ef4f7b840031 --- /dev/null +++ b/x-pack/test/ui_capabilities/spaces_only/tests/saved_objects_management.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { mapValues } from 'lodash'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; +import { SavedObjectsManagementBuilder } from '../../common/saved_objects_management_builder'; +import { UICapabilitiesService } from '../../common/services/ui_capabilities'; +import { SpaceScenarios } from '../scenarios'; + +const savedObjectsManagementBuilder = new SavedObjectsManagementBuilder(true); + +// eslint-disable-next-line import/no-default-export +export default function savedObjectsManagementTests({ + getService, +}: KibanaFunctionalTestDefaultProviders) { + const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities'); + + describe('savedObjectsManagement', () => { + SpaceScenarios.forEach(scenario => { + it(`${scenario.name}`, async () => { + // spaces don't affect saved objects management, so we assert the same thing for every scenario + const uiCapabilities = await uiCapabilitiesService.get(null, scenario.id); + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('savedObjectsManagement'); + const expected = mapValues(uiCapabilities.value!.savedObjectsManagement, () => + savedObjectsManagementBuilder.uiCapabilities('all') + ); + expect(uiCapabilities.value!.savedObjectsManagement).to.eql(expected); + }); + }); + }); +}