From 25039957f5a801e1567936d4cb092a18e5da6a98 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Sat, 5 Jan 2019 08:41:53 -0500 Subject: [PATCH] =?UTF-8?q?Enables=20the=20feature=20catalogue=20registry?= =?UTF-8?q?=20to=20be=20controlled=20via=20uiCapabil=E2=80=A6=20(#27945)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Enables the feature catalogue registry to be controlled via uiCapabilities * update snapshot * xpack_main populates uiCapabilities with the full list of catalogue entries * builds application privileges using catalogue actions * prevent 'catalogue' from being registered as a feature id * fix mocha tests --- src/legacy/core_plugins/kibana/index.js | 9 ++ src/ui/public/capabilities/ui_capabilities.ts | 1 + src/ui/public/registry/feature_catalogue.js | 6 +- .../public/registry/feature_catalogue.test.js | 102 ++++++++++++++++++ x-pack/plugins/__mocks__/ui/capabilities.ts | 1 + x-pack/plugins/apm/index.js | 1 + x-pack/plugins/canvas/init.js | 2 + x-pack/plugins/graph/index.js | 2 + x-pack/plugins/infra/server/kibana.index.ts | 2 + x-pack/plugins/ml/index.js | 1 + x-pack/plugins/monitoring/init.js | 1 + .../kibana_privileges.test.tsx.snap | 1 + .../kibana/kibana_privileges.test.tsx | 1 + .../lib/authorization/actions/ui.test.ts | 7 ++ .../server/lib/authorization/actions/ui.ts | 1 + .../disable_ui_capabilities.test.ts | 11 ++ .../features_privileges_builder.ts | 26 +++++ .../features_privileges_builders.test.ts | 88 +++++++++++++++ .../lib/authorization/privileges.test.ts | 7 ++ .../server/lib/authorization/privileges.ts | 2 + .../components/manage_spaces_button.test.tsx | 2 + .../secure_space_message.test.tsx | 2 + .../server/lib/toggle_ui_capabilities.test.ts | 13 ++- .../server/lib/toggle_ui_capabilities.ts | 10 +- .../lib/__tests__/replace_injected_vars.js | 14 ++- .../feature_registry.test.ts.snap | 2 + .../feature_registry/feature_registry.test.ts | 2 +- .../lib/feature_registry/feature_registry.ts | 4 +- .../lib/populate_ui_capabilities.test.ts | 78 +++++++++++++- .../server/lib/populate_ui_capabilities.ts | 54 +++++++--- .../server/lib/register_oss_features.ts | 11 ++ 31 files changed, 439 insertions(+), 25 deletions(-) create mode 100644 src/ui/public/registry/feature_catalogue.test.js diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index e529d9e50440d..cf42e807fd930 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -165,6 +165,15 @@ export default function (kibana) { dashboard: { showWriteControls: true }, + catalogue: { + discover: true, + dashboard: true, + visualize: true, + console: true, + advanced_settings: true, + saved_objects: true, + index_patterns: true, + }, management: { /* * Management settings correspond to management section/link ids, and should not be changed diff --git a/src/ui/public/capabilities/ui_capabilities.ts b/src/ui/public/capabilities/ui_capabilities.ts index 2080c3293557a..acb721fed0310 100644 --- a/src/ui/public/capabilities/ui_capabilities.ts +++ b/src/ui/public/capabilities/ui_capabilities.ts @@ -24,6 +24,7 @@ export interface UICapabilities { management: { [sectionId: string]: Record; }; + catalogue: Record; [key: string]: Record>; } diff --git a/src/ui/public/registry/feature_catalogue.js b/src/ui/public/registry/feature_catalogue.js index af8a68d69a7d6..e9b8742f94b01 100644 --- a/src/ui/public/registry/feature_catalogue.js +++ b/src/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/ui/public/registry/feature_catalogue.test.js b/src/ui/public/registry/feature_catalogue.test.js new file mode 100644 index 0000000000000..73f9a7d1fbe7f --- /dev/null +++ b/src/ui/public/registry/feature_catalogue.test.js @@ -0,0 +1,102 @@ +/* + * 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/chrome', () => ({ + getInjected: key => { + if (key === 'uiCapabilities') { + return { + navLinks: {}, + management: {}, + catalogue: { + item1: true, + item2: false, + item3: true, + }, + }; + } + throw new Error(`Unexpected call to chrome.getInjected with key ${key}`); + }, +})); +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/x-pack/plugins/__mocks__/ui/capabilities.ts b/x-pack/plugins/__mocks__/ui/capabilities.ts index b5c2053665b4a..5de2c6144fc6c 100644 --- a/x-pack/plugins/__mocks__/ui/capabilities.ts +++ b/x-pack/plugins/__mocks__/ui/capabilities.ts @@ -9,6 +9,7 @@ import { UICapabilities } from 'ui/capabilities'; let internals: UICapabilities = { navLinks: {}, management: {}, + catalogue: {}, spaces: { manage: true, }, diff --git a/x-pack/plugins/apm/index.js b/x-pack/plugins/apm/index.js index bbe8360daf100..086afa38336e1 100644 --- a/x-pack/plugins/apm/index.js +++ b/x-pack/plugins/apm/index.js @@ -76,6 +76,7 @@ export function apm(kibana) { navLinkId: 'apm', privileges: { all: { + catalogue: ['apm'], app: ['apm'], savedObject: { all: [], diff --git a/x-pack/plugins/canvas/init.js b/x-pack/plugins/canvas/init.js index 88f57f43e4e2b..240064f264e56 100644 --- a/x-pack/plugins/canvas/init.js +++ b/x-pack/plugins/canvas/init.js @@ -42,6 +42,7 @@ export default async function(server /*options*/) { navLinkId: 'canvas', privileges: { all: { + catalogue: ['canvas'], app: ['canvas'], savedObject: { all: ['canvas'], @@ -50,6 +51,7 @@ export default async function(server /*options*/) { ui: [], }, read: { + catalogue: ['canvas'], app: ['canvas'], savedObject: { all: [], diff --git a/x-pack/plugins/graph/index.js b/x-pack/plugins/graph/index.js index 1390595a9cbad..d729f7a99f7c4 100644 --- a/x-pack/plugins/graph/index.js +++ b/x-pack/plugins/graph/index.js @@ -57,6 +57,7 @@ export function graph(kibana) { navLinkId: 'graph', privileges: { all: { + catalogue: ['graph'], app: ['graph'], savedObject: { all: ['graph-workspace'], @@ -65,6 +66,7 @@ export function graph(kibana) { ui: [], }, read: { + catalogue: ['graph'], app: ['graph'], savedObject: { all: [], diff --git a/x-pack/plugins/infra/server/kibana.index.ts b/x-pack/plugins/infra/server/kibana.index.ts index f314923100314..83533ce9bd4ef 100644 --- a/x-pack/plugins/infra/server/kibana.index.ts +++ b/x-pack/plugins/infra/server/kibana.index.ts @@ -37,6 +37,7 @@ export const initServerWithKibana = (kbnServer: KbnServer) => { navLinkId: 'infra:home', privileges: { all: { + catalogue: ['infraops'], app: ['infra'], savedObject: { all: [], @@ -54,6 +55,7 @@ export const initServerWithKibana = (kbnServer: KbnServer) => { navLinkId: 'infra:logs', privileges: { all: { + catalogue: ['infralogging'], app: ['infra'], savedObject: { all: [], diff --git a/x-pack/plugins/ml/index.js b/x-pack/plugins/ml/index.js index 85b918621b0ef..9723fad6433a8 100644 --- a/x-pack/plugins/ml/index.js +++ b/x-pack/plugins/ml/index.js @@ -78,6 +78,7 @@ export const ml = (kibana) => { defaultMessage: 'The machine_learning_user or machine_learning_admin role should be assigned to grant access' }) }, + catalogue: ['ml'], app: ['ml'], savedObject: { all: [], diff --git a/x-pack/plugins/monitoring/init.js b/x-pack/plugins/monitoring/init.js index fc914d280a03d..b1fd3cdc54507 100644 --- a/x-pack/plugins/monitoring/init.js +++ b/x-pack/plugins/monitoring/init.js @@ -67,6 +67,7 @@ export const init = (monitoringPlugin, server) => { defaultMessage: 'The monitoring_user role should be assigned to grant access' }) }, + catalogue: ['monitoring'], app: ['monitoring'], savedObject: { all: [], diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/kibana_privileges.test.tsx.snap b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/kibana_privileges.test.tsx.snap index f8522f8075d77..cc5c922748c22 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/kibana_privileges.test.tsx.snap +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/kibana_privileges.test.tsx.snap @@ -44,6 +44,7 @@ exports[` renders without crashing 1`] = ` } uiCapabilities={ Object { + "catalogue": Object {}, "management": Object {}, "navLinks": Object {}, "spaces": Object { diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/kibana_privileges.test.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/kibana_privileges.test.tsx index 3b35bea9918b8..a4694973e7fb6 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/kibana_privileges.test.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/kibana_privileges.test.tsx @@ -43,6 +43,7 @@ const buildProps = (customProps = {}) => { uiCapabilities: { navLinks: {}, management: {}, + catalogue: {}, spaces: { manage: true, }, diff --git a/x-pack/plugins/security/server/lib/authorization/actions/ui.test.ts b/x-pack/plugins/security/server/lib/authorization/actions/ui.test.ts index 5ad59fe8d815f..71d70c4d0f60e 100644 --- a/x-pack/plugins/security/server/lib/authorization/actions/ui.test.ts +++ b/x-pack/plugins/security/server/lib/authorization/actions/ui.test.ts @@ -20,6 +20,13 @@ describe('#allNavlinks', () => { }); }); +describe('#allCatalogueEntries', () => { + test('returns ui:catalogue/*', () => { + const uiActions = new UIActions(); + expect(uiActions.allCatalogueEntries).toBe('ui:catalogue/*'); + }); +}); + describe('#get', () => { [null, undefined, '', 1, true, {}].forEach((featureId: any) => { test(`featureId of ${JSON.stringify(featureId)} throws error`, () => { diff --git a/x-pack/plugins/security/server/lib/authorization/actions/ui.ts b/x-pack/plugins/security/server/lib/authorization/actions/ui.ts index 4cebe038a149e..de9c43bfdd987 100644 --- a/x-pack/plugins/security/server/lib/authorization/actions/ui.ts +++ b/x-pack/plugins/security/server/lib/authorization/actions/ui.ts @@ -11,6 +11,7 @@ const prefix = 'ui:'; export class UIActions { public all = `${prefix}*`; public allNavLinks = `${prefix}navLinks/*`; + public allCatalogueEntries = `${prefix}catalogue/*`; public allManagementLinks = `${prefix}management/*`; public get(featureId: keyof UICapabilities, ...uiCapabilityParts: string[]) { diff --git a/x-pack/plugins/security/server/lib/authorization/disable_ui_capabilities.test.ts b/x-pack/plugins/security/server/lib/authorization/disable_ui_capabilities.test.ts index ea8e4051cc3e6..2164a06d01044 100644 --- a/x-pack/plugins/security/server/lib/authorization/disable_ui_capabilities.test.ts +++ b/x-pack/plugins/security/server/lib/authorization/disable_ui_capabilities.test.ts @@ -88,6 +88,7 @@ describe('usingPrivileges', () => { indices: true, }, }, + catalogue: {}, fooFeature: { foo: true, bar: true, @@ -109,6 +110,7 @@ describe('usingPrivileges', () => { indices: false, }, }, + catalogue: {}, fooFeature: { foo: false, bar: false, @@ -169,6 +171,7 @@ describe('usingPrivileges', () => { indices: true, }, }, + catalogue: {}, fooFeature: { foo: true, bar: true, @@ -190,6 +193,7 @@ describe('usingPrivileges', () => { indices: false, }, }, + catalogue: {}, fooFeature: { foo: false, bar: false, @@ -239,6 +243,7 @@ describe('usingPrivileges', () => { indices: true, }, }, + catalogue: {}, }) ).rejects.toThrowErrorMatchingSnapshot(); expect(mockServer.log).not.toHaveBeenCalled(); @@ -291,6 +296,7 @@ describe('usingPrivileges', () => { settings: false, }, }, + catalogue: {}, fooFeature: { foo: true, bar: true, @@ -314,6 +320,7 @@ describe('usingPrivileges', () => { settings: false, }, }, + catalogue: {}, fooFeature: { foo: true, bar: false, @@ -367,6 +374,7 @@ describe('usingPrivileges', () => { indices: false, }, }, + catalogue: {}, fooFeature: { foo: false, bar: false, @@ -388,6 +396,7 @@ describe('usingPrivileges', () => { indices: false, }, }, + catalogue: {}, fooFeature: { foo: false, bar: false, @@ -427,6 +436,7 @@ describe('all', () => { indices: true, }, }, + catalogue: {}, fooFeature: { foo: true, bar: true, @@ -447,6 +457,7 @@ describe('all', () => { indices: false, }, }, + catalogue: {}, fooFeature: { foo: false, bar: false, diff --git a/x-pack/plugins/security/server/lib/authorization/features_privileges_builder.ts b/x-pack/plugins/security/server/lib/authorization/features_privileges_builder.ts index 4c9baf3d79d3c..2216ff87bd778 100644 --- a/x-pack/plugins/security/server/lib/authorization/features_privileges_builder.ts +++ b/x-pack/plugins/security/server/lib/authorization/features_privileges_builder.ts @@ -53,6 +53,19 @@ export class FeaturesPrivilegesBuilder { ); } + public getCatalogueReadActions(features: Feature[]): string[] { + return flatten( + features.map(feature => { + const { privileges } = feature; + if (!privileges || !privileges.read || !privileges.read.catalogue) { + return []; + } + + return this.buildCatalogueFeaturePrivileges(privileges.read); + }) + ); + } + public getManagementReadActions(features: Feature[]): string[] { return flatten( features.map(feature => { @@ -86,10 +99,23 @@ export class FeaturesPrivilegesBuilder { ), ...privilegeDefinition.ui.map(ui => this.actions.ui.get(feature.id, ui)), ...(feature.navLinkId ? [this.actions.ui.get('navLinks', feature.navLinkId)] : []), + ...this.buildCatalogueFeaturePrivileges(privilegeDefinition), ...this.buildManagementFeaturePrivileges(privilegeDefinition), ]); } + private buildCatalogueFeaturePrivileges( + privilegeDefinition: FeaturePrivilegeDefinition + ): string[] { + if (!privilegeDefinition.catalogue) { + return []; + } + + return privilegeDefinition.catalogue.map(catalogueEntryId => + this.actions.ui.get('catalogue', catalogueEntryId) + ); + } + private buildManagementFeaturePrivileges( privilegeDefinition: FeaturePrivilegeDefinition ): string[] { diff --git a/x-pack/plugins/security/server/lib/authorization/features_privileges_builders.test.ts b/x-pack/plugins/security/server/lib/authorization/features_privileges_builders.test.ts index fd34fa0343564..fea02ab88eeea 100644 --- a/x-pack/plugins/security/server/lib/authorization/features_privileges_builders.test.ts +++ b/x-pack/plugins/security/server/lib/authorization/features_privileges_builders.test.ts @@ -124,6 +124,39 @@ describe('#buildFeaturesPrivileges', () => { }); }); + test('includes catalogue actions when specified', () => { + const actions = new Actions(versionNumber); + const builder = new FeaturesPrivilegesBuilder(actions); + const features = [ + { + id: 'foo', + name: '', + privileges: { + bar: { + catalogue: ['fooEntry', 'barEntry'], + app: [], + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + }, + }, + ]; + const result = builder.buildFeaturesPrivileges(features); + expect(result).toEqual({ + foo: { + bar: [ + actions.login, + actions.version, + actions.ui.get('catalogue', 'fooEntry'), + actions.ui.get('catalogue', 'barEntry'), + ], + }, + }); + }); + test('includes savedObject all actions when specified', () => { const actions = new Actions(versionNumber); const builder = new FeaturesPrivilegesBuilder(actions); @@ -406,3 +439,58 @@ describe('#getManagementReadActions', () => { ]); }); }); + +describe('#getCatalogueReadActions', () => { + test(`includes catalogue actions from the read privileges`, () => { + const actions = new Actions(versionNumber); + const builder = new FeaturesPrivilegesBuilder(actions); + const features: Feature[] = [ + { + id: 'foo', + name: '', + privileges: { + // wrong privilege name + bar: { + catalogue: [], + app: [], + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + // no catalogue read privileges + read: { + app: [], + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + }, + }, + { + id: 'bar', + name: '', + privileges: { + // this catalogue capability should show up in the results + read: { + app: [], + catalogue: ['barCatalogueLink', 'bazCatalogueLink'], + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + }, + }, + ]; + const result = builder.getCatalogueReadActions(features); + expect(result).toEqual([ + actions.ui.get('catalogue', 'barCatalogueLink'), + actions.ui.get('catalogue', 'bazCatalogueLink'), + ]); + }); +}); diff --git a/x-pack/plugins/security/server/lib/authorization/privileges.test.ts b/x-pack/plugins/security/server/lib/authorization/privileges.test.ts index 7ad69381b15d6..24211aceaa06a 100644 --- a/x-pack/plugins/security/server/lib/authorization/privileges.test.ts +++ b/x-pack/plugins/security/server/lib/authorization/privileges.test.ts @@ -24,6 +24,7 @@ test(`builds privileges correctly`, () => { privileges: { all: { app: ['foo-app'], + catalogue: ['fooAppEntry1', 'fooAppEntry2'], management: { foo: ['fooManagementLink'], }, @@ -36,6 +37,7 @@ test(`builds privileges correctly`, () => { read: { app: ['foo-app'], api: ['foo/read/api'], + catalogue: ['fooReadEntry'], management: { foo: ['anotherFooManagementLink'], }, @@ -136,6 +138,8 @@ test(`builds privileges correctly`, () => { 'ui:foo-feature/showSaveButton', 'ui:foo-feature/showCreateButton', 'ui:navLinks/kibana:foo-feature', + 'ui:catalogue/fooAppEntry1', + 'ui:catalogue/fooAppEntry2', 'ui:management/foo/fooManagementLink', ], read: [ @@ -151,6 +155,7 @@ test(`builds privileges correctly`, () => { 'saved_object:bar-saved-object-type/find', 'ui:foo-feature/show', 'ui:navLinks/kibana:foo-feature', + 'ui:catalogue/fooReadEntry', 'ui:management/foo/anotherFooManagementLink', ], }, @@ -180,6 +185,7 @@ test(`builds privileges correctly`, () => { 'ui:foo-feature/show', 'ui:bar-feature/show', 'ui:management/foo/anotherFooManagementLink', + 'ui:catalogue/fooReadEntry', 'ui:navLinks/*', ], }, @@ -220,6 +226,7 @@ test(`builds privileges correctly`, () => { 'ui:foo-feature/show', 'ui:bar-feature/show', 'ui:management/foo/anotherFooManagementLink', + 'ui:catalogue/fooReadEntry', 'ui:navLinks/*', ], }, diff --git a/x-pack/plugins/security/server/lib/authorization/privileges.ts b/x-pack/plugins/security/server/lib/authorization/privileges.ts index 1d24a091c7795..4acd538f23c1f 100644 --- a/x-pack/plugins/security/server/lib/authorization/privileges.ts +++ b/x-pack/plugins/security/server/lib/authorization/privileges.ts @@ -58,6 +58,7 @@ export function privilegesFactory( ...actions.savedObject.readOperations(validSavedObjectTypes), ...featuresPrivilegesBuilder.getUIReadActions(features), ...featuresPrivilegesBuilder.getManagementReadActions(features), + ...featuresPrivilegesBuilder.getCatalogueReadActions(features), actions.ui.allNavLinks, ], }, @@ -78,6 +79,7 @@ export function privilegesFactory( ...actions.savedObject.readOperations(validSavedObjectTypes), ...featuresPrivilegesBuilder.getUIReadActions(features), ...featuresPrivilegesBuilder.getManagementReadActions(features), + ...featuresPrivilegesBuilder.getCatalogueReadActions(features), actions.ui.allNavLinks, ], }, diff --git a/x-pack/plugins/spaces/public/components/manage_spaces_button.test.tsx b/x-pack/plugins/spaces/public/components/manage_spaces_button.test.tsx index 5fe3bf4b473cc..4d59aa19e7f40 100644 --- a/x-pack/plugins/spaces/public/components/manage_spaces_button.test.tsx +++ b/x-pack/plugins/spaces/public/components/manage_spaces_button.test.tsx @@ -13,6 +13,7 @@ describe('ManageSpacesButton', () => { setMockCapabilities({ navLinks: {}, management: {}, + catalogue: {}, spaces: { manage: true, }, @@ -26,6 +27,7 @@ describe('ManageSpacesButton', () => { setMockCapabilities({ navLinks: {}, management: {}, + catalogue: {}, spaces: { manage: false, }, diff --git a/x-pack/plugins/spaces/public/views/management/components/secure_space_message/secure_space_message.test.tsx b/x-pack/plugins/spaces/public/views/management/components/secure_space_message/secure_space_message.test.tsx index 297a887f77ec7..57ceaea2d4943 100644 --- a/x-pack/plugins/spaces/public/views/management/components/secure_space_message/secure_space_message.test.tsx +++ b/x-pack/plugins/spaces/public/views/management/components/secure_space_message/secure_space_message.test.tsx @@ -13,6 +13,7 @@ describe('SecureSpaceMessage', () => { setMockCapabilities({ navLinks: {}, management: {}, + catalogue: {}, spaces: { manage: false }, }); expect(shallowWithIntl()).toMatchSnapshot(); @@ -22,6 +23,7 @@ describe('SecureSpaceMessage', () => { setMockCapabilities({ navLinks: {}, management: {}, + catalogue: {}, spaces: { manage: true }, }); expect(shallowWithIntl()).toMatchSnapshot(); 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 index 09a8893d58ff5..92ee18342f60e 100644 --- a/x-pack/plugins/spaces/server/lib/toggle_ui_capabilities.test.ts +++ b/x-pack/plugins/spaces/server/lib/toggle_ui_capabilities.test.ts @@ -21,6 +21,7 @@ const features: Feature[] = [ navLinkId: 'feature2', privileges: { all: { + catalogue: ['feature2Entry'], management: { kibana: ['somethingElse'], }, @@ -39,6 +40,7 @@ const features: Feature[] = [ navLinkId: 'feature3', privileges: { all: { + catalogue: ['feature3Entry'], management: { kibana: ['indices'], }, @@ -61,6 +63,10 @@ const buildUiCapabilities = () => feature3: true, unknownFeature: true, }, + catalogue: { + discover: true, + visualize: false, + }, management: { kibana: { settings: false, @@ -80,7 +86,7 @@ const buildUiCapabilities = () => foo: true, bar: true, }, - }); + }) as UICapabilities; describe('toggleUiCapabilities', () => { it('does not toggle capabilities when the space has no disabled features', () => { @@ -107,7 +113,7 @@ describe('toggleUiCapabilities', () => { expect(result).toEqual(buildUiCapabilities()); }); - it('disables the corresponding navLink, management sections, and all capability flags for disabled features', () => { + it('disables the corresponding navLink, catalogue, management sections, and all capability flags for disabled features', () => { const space: Space = { id: 'space', name: '', @@ -120,6 +126,7 @@ describe('toggleUiCapabilities', () => { 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; @@ -143,11 +150,13 @@ describe('toggleUiCapabilities', () => { 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; diff --git a/x-pack/plugins/spaces/server/lib/toggle_ui_capabilities.ts b/x-pack/plugins/spaces/server/lib/toggle_ui_capabilities.ts index ebe85155115ef..b9c3765d37ecf 100644 --- a/x-pack/plugins/spaces/server/lib/toggle_ui_capabilities.ts +++ b/x-pack/plugins/spaces/server/lib/toggle_ui_capabilities.ts @@ -32,6 +32,7 @@ function toggleDisabledFeatures( .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) { @@ -40,10 +41,15 @@ function toggleDisabledFeatures( navLinks[feature.navLinkId] = false; } - // Disable associated management items Object.values(feature.privileges).forEach(privilege => { - const privilegeManagementSections: Record = privilege.management || {}; + // Disable associated catalogue entries + const privilegeCatalogueEntries: string[] = privilege.catalogue || []; + privilegeCatalogueEntries.forEach(catalogueEntryId => { + catalogueEntries[catalogueEntryId] = false; + }); + // Disable associated management items + const privilegeManagementSections: Record = privilege.management || {}; Object.entries(privilegeManagementSections).forEach(([sectionId, sectionItems]) => { sectionItems.forEach(item => { if ( 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 ccde29dd95864..88c7664bd2fe4 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 @@ -51,6 +51,7 @@ describe('replaceInjectedVars uiExport', () => { mockFeature: { mockFeatureCapability: true, }, + catalogue: {} }, }); @@ -75,6 +76,7 @@ describe('replaceInjectedVars uiExport', () => { mockFeature: { mockFeatureCapability: true, }, + catalogue: {} }, }); }); @@ -96,6 +98,7 @@ describe('replaceInjectedVars uiExport', () => { mockFeature: { mockFeatureCapability: true, }, + catalogue: {} }, }); }); @@ -117,6 +120,7 @@ describe('replaceInjectedVars uiExport', () => { mockFeature: { mockFeatureCapability: true, }, + catalogue: {} }, }); }); @@ -138,6 +142,7 @@ describe('replaceInjectedVars uiExport', () => { mockFeature: { mockFeatureCapability: true, }, + catalogue: {} }, }); }); @@ -159,6 +164,7 @@ describe('replaceInjectedVars uiExport', () => { mockFeature: { mockFeatureCapability: true, }, + catalogue: {} }, }); }); @@ -176,6 +182,7 @@ describe('replaceInjectedVars uiExport', () => { mockFeature: { mockFeatureCapability: true, }, + catalogue: {} }, }); }); @@ -193,12 +200,13 @@ describe('replaceInjectedVars uiExport', () => { mockFeature: { mockFeatureCapability: true, }, + catalogue: {} }, }); }); it('sends the originalInjectedVars (with xpackInitialInfo = undefined) if security is disabled, xpack info is unavailable', async () => { - const originalInjectedVars = { a: 1, uiCapabilities: { navLinks: { foo: true }, bar: { baz: true } } }; + const originalInjectedVars = { a: 1, uiCapabilities: { navLinks: { foo: true }, bar: { baz: true }, catalogue: { cfoo: true } } }; const request = buildRequest(); const server = mockServer(); delete server.plugins.security; @@ -215,6 +223,9 @@ describe('replaceInjectedVars uiExport', () => { mockFeature: { mockFeatureCapability: true, }, + catalogue: { + cfoo: true, + } }, }); }); @@ -232,6 +243,7 @@ describe('replaceInjectedVars uiExport', () => { mockFeature: { mockFeatureCapability: true, }, + catalogue: {} }, }); }); 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 index 08ee40672708d..1b465da3d0414 100644 --- 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 @@ -1,5 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`registerFeature prevents features from being registered with an ID of "catalogue" 1`] = `"child \\"id\\" fails because [\\"id\\" contains an invalid value]"`; + exports[`registerFeature 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[`registerFeature prevents features from being registered with an ID of "management" 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 index 38c1658470157..5e0656e0dfdb1 100644 --- 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 @@ -82,7 +82,7 @@ describe('registerFeature', () => { ); }); - ['management', 'navLinks', `doesn't match valid regex`].forEach(prohibitedId => { + ['catalogue', 'management', 'navLinks', `doesn't match valid regex`].forEach(prohibitedId => { it(`prevents features from being registered with an ID of "${prohibitedId}"`, () => { expect(() => registerFeature({ 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 index c0457f50c4775..0ea97af522036 100644 --- 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 @@ -16,6 +16,7 @@ export interface FeaturePrivilegeDefinition { management?: { [sectionId: string]: string[]; }; + catalogue?: string[]; api?: string[]; app: string[]; savedObject: { @@ -39,7 +40,7 @@ export interface Feature { // 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 = ['management', 'navLinks']; +const prohibitedFeatureIds: Array = ['catalogue', 'management', 'navLinks']; const featurePrivilegePartRegex = /^[a-zA-Z0-9_-]+$/; const managementSectionIdRegex = /^[a-zA-Z0-9_-]+$/; @@ -63,6 +64,7 @@ const schema = Joi.object({ tooltip: Joi.string(), }), management: Joi.object().pattern(managementSectionIdRegex, Joi.array().items(Joi.string())), + catalogue: Joi.array().items(Joi.string()), api: Joi.array().items(Joi.string()), app: Joi.array() .items(Joi.string()) 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 index 45031c8329f5c..14f06367cc6c9 100644 --- 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 @@ -22,6 +22,10 @@ function getMockOriginalInjectedVars() { bar: true, }, management: {}, + catalogue: { + fooEntry: true, + barEntry: true, + }, feature: { someCapability: true, }, @@ -30,7 +34,11 @@ function getMockOriginalInjectedVars() { }; } -function createFeaturePrivilege(key: string, capabilities: string[] = []) { +function createFeaturePrivilege( + key: string, + capabilities: string[] = [], + catalogueEntries: string[] = [] +) { return { [key]: { savedObject: { @@ -38,6 +46,7 @@ function createFeaturePrivilege(key: string, capabilities: string[] = []) { read: [], }, app: [], + catalogue: catalogueEntries, ui: [...capabilities], }, }; @@ -63,6 +72,10 @@ describe('populateUICapabilities', () => { bar: true, }, management: {}, + catalogue: { + fooEntry: true, + barEntry: true, + }, otherFeature: {}, }); }); @@ -88,6 +101,10 @@ describe('populateUICapabilities', () => { bar: true, }, management: {}, + catalogue: { + fooEntry: true, + barEntry: true, + }, newFeature: {}, otherFeature: {}, }); @@ -115,9 +132,60 @@ describe('populateUICapabilities', () => { 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', + privileges: { + ...createFeaturePrivilege('foo', ['capability1', 'capability2'], ['anotherFooEntry']), + ...createFeaturePrivilege('bar', ['capability3', 'capability4'], ['anotherBarEntry']), + ...createFeaturePrivilege( + 'baz', + ['capability1', 'capability5'], + ['aBazEntry', 'anotherBazEntry'] + ), + }, + }, + ]); + 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, + aBazEntry: true, + anotherBazEntry: true, + }, newFeature: { capability1: true, capability2: true, + capability3: true, + capability4: true, + capability5: true, }, otherFeature: {}, }); @@ -147,6 +215,10 @@ describe('populateUICapabilities', () => { bar: true, }, management: {}, + catalogue: { + fooEntry: true, + barEntry: true, + }, newFeature: { capability1: true, capability2: true, @@ -210,6 +282,10 @@ describe('populateUICapabilities', () => { bar: true, }, management: {}, + catalogue: { + fooEntry: true, + barEntry: true, + }, newFeature: { capability1: true, capability2: 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 index 42f07569b7cba..b6e532700a229 100644 --- a/x-pack/plugins/xpack_main/server/lib/populate_ui_capabilities.ts +++ b/x-pack/plugins/xpack_main/server/lib/populate_ui_capabilities.ts @@ -8,6 +8,8 @@ import _ from 'lodash'; import { UICapabilities } from 'ui/capabilities'; import { Feature } from '../../types'; +const ELIGIBLE_FLAT_MERGE_KEYS = ['catalogue']; + interface FeatureCapabilities { [featureId: string]: Record; } @@ -24,29 +26,38 @@ export function populateUICapabilities( } function getCapabilitiesFromFeature(feature: Feature): FeatureCapabilities { - const capabilities: FeatureCapabilities = { + const UIFeatureCapabilities: FeatureCapabilities = { + catalogue: {}, [feature.id]: {}, }; - const featureCapabilities: Record = Object.values(feature.privileges).reduce( - (acc, privilege) => { - return { - ...acc, - ...privilege.ui.reduce( - (privilegeAcc, capabillity) => ({ + Object.values(feature.privileges).forEach(privilege => { + UIFeatureCapabilities[feature.id] = { + ...UIFeatureCapabilities[feature.id], + ...privilege.ui.reduce( + (privilegeAcc, capability) => ({ + ...privilegeAcc, + [capability]: true, + }), + {} + ), + }; + + if (privilege.catalogue) { + UIFeatureCapabilities.catalogue = { + ...UIFeatureCapabilities.catalogue, + ...privilege.catalogue.reduce( + (privilegeAcc, capability) => ({ ...privilegeAcc, - [capabillity]: true, + [capability]: true, }), {} ), }; - }, - {} - ); - - capabilities[feature.id] = featureCapabilities; + } + }); - return capabilities; + return UIFeatureCapabilities; } function mergeCapabilities( @@ -54,9 +65,20 @@ function mergeCapabilities( ...allFeatureCapabilities: FeatureCapabilities[] ): UICapabilities { return allFeatureCapabilities.reduce((acc, capabilities) => { - return { - ...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 index b74b4de04b4e1..880e4211a25a5 100644 --- a/x-pack/plugins/xpack_main/server/lib/register_oss_features.ts +++ b/x-pack/plugins/xpack_main/server/lib/register_oss_features.ts @@ -14,6 +14,7 @@ const kibanaFeatures: Feature[] = [ navLinkId: 'kibana:discover', privileges: { all: { + catalogue: ['discover'], app: ['kibana'], savedObject: { all: ['search'], @@ -22,6 +23,7 @@ const kibanaFeatures: Feature[] = [ ui: [], }, read: { + catalogue: ['discover'], app: ['kibana'], savedObject: { all: [], @@ -38,6 +40,7 @@ const kibanaFeatures: Feature[] = [ navLinkId: 'kibana:visualize', privileges: { all: { + catalogue: ['visualize'], app: ['kibana'], savedObject: { all: ['visualization'], @@ -46,6 +49,7 @@ const kibanaFeatures: Feature[] = [ ui: [], }, read: { + catalogue: ['visualize'], app: ['kibana'], savedObject: { all: [], @@ -62,6 +66,7 @@ const kibanaFeatures: Feature[] = [ navLinkId: 'kibana:dashboard', privileges: { all: { + catalogue: ['dashboard'], app: ['kibana'], savedObject: { all: ['dashboard'], @@ -70,6 +75,7 @@ const kibanaFeatures: Feature[] = [ ui: [], }, read: { + catalogue: ['dashboard'], app: ['kibana'], savedObject: { all: [], @@ -94,6 +100,7 @@ const kibanaFeatures: Feature[] = [ navLinkId: 'kibana:dev_tools', privileges: { all: { + catalogue: ['console', 'searchprofiler', 'grokdebugger'], api: ['console/execute'], app: ['kibana'], savedObject: { @@ -110,6 +117,7 @@ const kibanaFeatures: Feature[] = [ icon: 'advancedSettingsApp', privileges: { all: { + catalogue: ['advanced_settings'], management: { kibana: ['settings'], }, @@ -128,6 +136,7 @@ const kibanaFeatures: Feature[] = [ icon: 'indexPatternApp', privileges: { all: { + catalogue: ['index_patterns'], management: { kibana: ['indices'], }, @@ -150,6 +159,7 @@ const timelionFeatures: Feature[] = [ navLinkId: 'timelion', privileges: { all: { + catalogue: ['timelion'], app: ['timelion'], savedObject: { all: ['timelion'], @@ -158,6 +168,7 @@ const timelionFeatures: Feature[] = [ ui: [], }, read: { + catalogue: ['timelion'], app: ['timelion'], savedObject: { all: [],