From 2e7bbd1abf5e2b97c35ba279c16e1b20a5b6d5c5 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Mon, 5 Nov 2018 15:23:55 -0500 Subject: [PATCH] Restructure user profile for granular app privs (#23750) merging to feature branch for further development --- .../kibana/public/discover/discover_config.js | 32 +++++++++++ .../kibana/public/discover/index.js | 1 + src/ui/public/chrome/api/config.js | 34 ++++++++++++ src/ui/public/chrome/chrome.js | 2 + x-pack/plugins/security/index.js | 4 ++ .../kibana/kibana_privileges.test.tsx | 1 + .../views/management/edit_role/index.js | 2 +- .../public/views/management/page_routes.tsx | 10 +++- .../public/views/nav_control/nav_control.tsx | 3 +- .../nav_control/nav_control_popover.test.tsx | 1 + x-pack/plugins/xpack_main/common/index.ts | 7 +++ .../common/user_profile/capabilities.ts | 9 +++ .../xpack_main/common/user_profile/index.ts | 7 +++ .../common/user_profile/user_profile.ts | 38 +++++++++++++ x-pack/plugins/xpack_main/index.js | 5 ++ .../xpack_main/public/hacks/user_profile.ts | 17 ++++++ .../hacks/user_profile_config_decorators.js | 52 ++++++++++++++++++ .../lib/__tests__/replace_injected_vars.js | 3 + .../feature_registry/register_oss_features.ts | 54 ++++++++++++++++++ .../server/lib/user_profile/index.ts | 8 +++ .../lib/user_profile/priority_collection.ts | 31 +++++++++++ .../user_profile_capability_registry.ts | 55 +++++++++++++++++++ .../lib/user_profile/user_profile_mixin.ts | 28 ++++++++++ 23 files changed, 399 insertions(+), 5 deletions(-) create mode 100644 src/core_plugins/kibana/public/discover/discover_config.js create mode 100644 src/ui/public/chrome/api/config.js create mode 100644 x-pack/plugins/xpack_main/common/index.ts create mode 100644 x-pack/plugins/xpack_main/common/user_profile/capabilities.ts create mode 100644 x-pack/plugins/xpack_main/common/user_profile/index.ts create mode 100644 x-pack/plugins/xpack_main/common/user_profile/user_profile.ts create mode 100644 x-pack/plugins/xpack_main/public/hacks/user_profile.ts create mode 100644 x-pack/plugins/xpack_main/public/hacks/user_profile_config_decorators.js create mode 100644 x-pack/plugins/xpack_main/server/lib/feature_registry/register_oss_features.ts create mode 100644 x-pack/plugins/xpack_main/server/lib/user_profile/index.ts create mode 100644 x-pack/plugins/xpack_main/server/lib/user_profile/priority_collection.ts create mode 100644 x-pack/plugins/xpack_main/server/lib/user_profile/user_profile_capability_registry.ts create mode 100644 x-pack/plugins/xpack_main/server/lib/user_profile/user_profile_mixin.ts diff --git a/src/core_plugins/kibana/public/discover/discover_config.js b/src/core_plugins/kibana/public/discover/discover_config.js new file mode 100644 index 000000000000000..d98a3093704817d --- /dev/null +++ b/src/core_plugins/kibana/public/discover/discover_config.js @@ -0,0 +1,32 @@ +/* + * 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 { uiModules } from 'ui/modules'; + +uiModules.get('kibana') + .provider('discoverConfig', () => { + return { + $get() { + return { + getHideWriteControls() { + return false; + } + }; + } + }; + }); diff --git a/src/core_plugins/kibana/public/discover/index.js b/src/core_plugins/kibana/public/discover/index.js index 5b23bbd4c01c6ff..45d62fed5a47ac8 100644 --- a/src/core_plugins/kibana/public/discover/index.js +++ b/src/core_plugins/kibana/public/discover/index.js @@ -17,6 +17,7 @@ * under the License. */ +import './discover_config'; import './saved_searches/saved_searches'; import './directives'; import 'ui/collapsible_sidebar'; diff --git a/src/ui/public/chrome/api/config.js b/src/ui/public/chrome/api/config.js new file mode 100644 index 000000000000000..fe12384c58a52cc --- /dev/null +++ b/src/ui/public/chrome/api/config.js @@ -0,0 +1,34 @@ +/* + * 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 { uiModules } from '../../modules'; + +export function initConfig() { + uiModules.get('kibana') + .provider('chromeConfig', () => { + return { + $get() { + return { + shouldHideNavLink() { + return false; + } + }; + } + }; + }); +} diff --git a/src/ui/public/chrome/chrome.js b/src/ui/public/chrome/chrome.js index 7eba9510768036f..d17155fe473d3c6 100644 --- a/src/ui/public/chrome/chrome.js +++ b/src/ui/public/chrome/chrome.js @@ -38,6 +38,7 @@ import { initChromeNavApi } from './api/nav'; import { initBreadcrumbsApi } from './api/breadcrumbs'; import templateApi from './api/template'; import { initChromeThemeApi } from './api/theme'; +import { initConfig } from './api/config'; import { initChromeXsrfApi } from './api/xsrf'; import { initUiSettingsApi } from './api/ui_settings'; import { initLoadingCountApi } from './api/loading_count'; @@ -61,6 +62,7 @@ const internals = _.defaults( } ); +initConfig(); initUiSettingsApi(chrome); initSavedObjectClient(chrome); appsApi(chrome, internals); diff --git a/x-pack/plugins/security/index.js b/x-pack/plugins/security/index.js index c4a7b9080fc10bd..fba44d136fa74ce 100644 --- a/x-pack/plugins/security/index.js +++ b/x-pack/plugins/security/index.js @@ -24,6 +24,8 @@ import { createAuthorizationService, registerPrivilegesWithCluster } from './ser 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 { capabilityDecorator } from './server/lib/capability_decorator'; +import { registerUserProfileCapabilityDecorator } from '../xpack_main/server/lib/user_profile'; export const security = (kibana) => new kibana.Plugin({ id: 'security', @@ -127,6 +129,8 @@ export const security = (kibana) => new kibana.Plugin({ } }); + registerUserProfileCapabilityDecorator(Number.MIN_SAFE_INTEGER, capabilityDecorator); + const auditLogger = new SecurityAuditLogger(server.config(), new AuditLogger(server, 'security')); savedObjects.setScopedSavedObjectsClientFactory(({ 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 27dfc263e57326e..2a7a1d3e00f4bc5 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 @@ -7,6 +7,7 @@ import { shallow } from 'enzyme'; import React from 'react'; import { KibanaPrivilege } from '../../../../../../../../security/common/model/kibana_privilege'; +import { UserProfile } from '../../../../../../../../xpack_main/common/user_profile'; import { RoleValidator } from '../../../lib/validate_role'; import { KibanaPrivileges } from './kibana_privileges'; import { SimplePrivilegeForm } from './simple_privilege_form'; diff --git a/x-pack/plugins/security/public/views/management/edit_role/index.js b/x-pack/plugins/security/public/views/management/edit_role/index.js index 9a9f01d16ef1a0d..373f1c4dce82aba 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/index.js +++ b/x-pack/plugins/security/public/views/management/edit_role/index.js @@ -87,7 +87,7 @@ routes.when(`${EDIT_ROLES_PATH}/:name?`, { } }, controllerAs: 'editRole', - controller($injector, $scope, $http, enableSpaceAwarePrivileges) { + controller($injector, $scope, $http, enableSpaceAwarePrivileges, userProfile) { const $route = $injector.get('$route'); const Private = $injector.get('Private'); diff --git a/x-pack/plugins/spaces/public/views/management/page_routes.tsx b/x-pack/plugins/spaces/public/views/management/page_routes.tsx index 1263d1beead4509..d76cf1d25aff8ed 100644 --- a/x-pack/plugins/spaces/public/views/management/page_routes.tsx +++ b/x-pack/plugins/spaces/public/views/management/page_routes.tsx @@ -14,6 +14,7 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; // @ts-ignore import routes from 'ui/routes'; +import { UserProfile } from '../../../../xpack_main/common/user_profile'; import { SpacesManager } from '../../lib/spaces_manager'; import { ManageSpacePage } from './edit_space'; import { SpacesGridPage } from './spaces_grid'; @@ -27,7 +28,8 @@ routes.when('/management/spaces/list', { $http: any, chrome: any, spacesNavState: SpacesNavState, - spaceSelectorURL: string + spaceSelectorURL: string, + userProfile: UserProfile ) { $scope.$$postDigest(() => { const domNode = document.getElementById(reactRootNodeId); @@ -58,7 +60,8 @@ routes.when('/management/spaces/create', { $http: any, chrome: any, spacesNavState: SpacesNavState, - spaceSelectorURL: string + spaceSelectorURL: string, + userProfile: UserProfile ) { $scope.$$postDigest(() => { const domNode = document.getElementById(reactRootNodeId); @@ -95,7 +98,8 @@ routes.when('/management/spaces/edit/:spaceId', { chrome: any, Private: any, spacesNavState: SpacesNavState, - spaceSelectorURL: string + spaceSelectorURL: string, + userProfile: UserProfile ) { $scope.$$postDigest(() => { const domNode = document.getElementById(reactRootNodeId); 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 180d9114ee40bf2..0c2dff2cbc71d61 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 @@ -21,6 +21,7 @@ import { uiModules } from 'ui/modules'; import { chromeHeaderNavControlsRegistry } from 'ui/registry/chrome_header_nav_controls'; // @ts-ignore import { chromeNavControlsRegistry } from 'ui/registry/chrome_nav_controls'; +import { UserProfile } from '../../../../xpack_main/common/user_profile'; import { Space } from '../../../common/model/space'; import { SpacesGlobalNavButton } from './components/spaces_global_nav_button'; import { SpacesHeaderNavButton } from './components/spaces_header_nav_button'; @@ -93,7 +94,7 @@ module.service('spacesNavState', (activeSpace: any) => { }); chromeHeaderNavControlsRegistry.register( - ($http: any, chrome: any, Private: any, activeSpace: any) => ({ + ($http: any, chrome: any, Private: any, activeSpace: any, userProfile: UserProfile) => ({ name: 'spaces', order: 1000, side: NavControlSide.Left, 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 1f743bfba7e5ae0..581ff88b26f413a 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 @@ -6,6 +6,7 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; +import { UserProfile } from '../../../../xpack_main/common/user_profile'; import { SpaceAvatar } from '../../components'; import { SpacesManager } from '../../lib/spaces_manager'; import { SpacesGlobalNavButton } from './components/spaces_global_nav_button'; diff --git a/x-pack/plugins/xpack_main/common/index.ts b/x-pack/plugins/xpack_main/common/index.ts new file mode 100644 index 000000000000000..23c27385776c2b6 --- /dev/null +++ b/x-pack/plugins/xpack_main/common/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 { Capabilities, UserProfile } from './user_profile'; diff --git a/x-pack/plugins/xpack_main/common/user_profile/capabilities.ts b/x-pack/plugins/xpack_main/common/user_profile/capabilities.ts new file mode 100644 index 000000000000000..c1e893d5a1bc1f2 --- /dev/null +++ b/x-pack/plugins/xpack_main/common/user_profile/capabilities.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 Capabilities { + [capability: string]: boolean; +} diff --git a/x-pack/plugins/xpack_main/common/user_profile/index.ts b/x-pack/plugins/xpack_main/common/user_profile/index.ts new file mode 100644 index 000000000000000..ef5c9c037ebaec9 --- /dev/null +++ b/x-pack/plugins/xpack_main/common/user_profile/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 { Capabilities } from './capabilities'; +export { UserProfile } from './user_profile'; diff --git a/x-pack/plugins/xpack_main/common/user_profile/user_profile.ts b/x-pack/plugins/xpack_main/common/user_profile/user_profile.ts new file mode 100644 index 000000000000000..7d657b0f3ccf91a --- /dev/null +++ b/x-pack/plugins/xpack_main/common/user_profile/user_profile.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 { Capabilities } from './capabilities'; + +export class 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; + } + + public canAccessFeature(feature: string, defaultValue: boolean = true): boolean { + return this.hasCapability(`ui:${feature}/read`, defaultValue); + } + + public canReadSavedObject(savedObjectType: string, defaultValue: boolean = true): boolean { + return this.hasCapability(`saved_objects/${savedObjectType}/get`, defaultValue); + } + + public canWriteSavedObject(savedObjectType: string, defaultValue: boolean = true): boolean { + return this.hasCapability(`saved_objects/${savedObjectType}/create`, defaultValue); + } + + public toJSON() { + return { + ...this.capabilities, + }; + } +} diff --git a/x-pack/plugins/xpack_main/index.js b/x-pack/plugins/xpack_main/index.js index 448a08f528b8c66..77a837b24dc152b 100644 --- a/x-pack/plugins/xpack_main/index.js +++ b/x-pack/plugins/xpack_main/index.js @@ -22,6 +22,7 @@ import { CONFIG_TELEMETRY_DESC, } from './common/constants'; import { settingsRoute } from './server/routes/api/v1/settings'; +import { userProfileMixin } from './server/lib/user_profile'; import mappings from './mappings.json'; export { callClusterFactory } from './server/lib/call_cluster_factory'; @@ -102,6 +103,8 @@ export const xpackMain = (kibana) => { 'plugins/xpack_main/hacks/check_xpack_info_change', 'plugins/xpack_main/hacks/telemetry_opt_in', 'plugins/xpack_main/hacks/telemetry_trigger', + 'plugins/xpack_main/hacks/user_profile', + 'plugins/xpack_main/hacks/user_profile_config_decorators', ], replaceInjectedVars, __webpackPluginProvider__(webpack) { @@ -122,6 +125,8 @@ export const xpackMain = (kibana) => { setupXPackMain(server); registerOssFeatures(); + userProfileMixin(this.kbnServer, server); + // register routes xpackInfoRoute(server); telemetryRoute(server); diff --git a/x-pack/plugins/xpack_main/public/hacks/user_profile.ts b/x-pack/plugins/xpack_main/public/hacks/user_profile.ts new file mode 100644 index 000000000000000..91cdcb419e545ba --- /dev/null +++ b/x-pack/plugins/xpack_main/public/hacks/user_profile.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 chrome from 'ui/chrome'; +// @ts-ignore +import { uiModules } from 'ui/modules'; +import { UserProfile } from '../../common'; + +uiModules.get('userProfile').provider('userProfile', function userProfileProvider() { + // @ts-ignore + this.$get = () => { + return new UserProfile(chrome.getInjected('userProfileData')); + }; +}); diff --git a/x-pack/plugins/xpack_main/public/hacks/user_profile_config_decorators.js b/x-pack/plugins/xpack_main/public/hacks/user_profile_config_decorators.js new file mode 100644 index 000000000000000..851a3a2e189501d --- /dev/null +++ b/x-pack/plugins/xpack_main/public/hacks/user_profile_config_decorators.js @@ -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. + */ + +import { uiModules } from 'ui/modules'; + +uiModules.get('kibana') + .config(($provide, $injector) => { + if ($injector.has('dashboardConfig')) { + $provide.decorator('dashboardConfig', function ($delegate, userProfile) { + return { + getHideWriteControls() { + if (!userProfile.canWriteSavedObject('dashboard')) { + return true; + } + + return $delegate.getHideWriteControls(); + } + }; + }); + } + + if ($injector.has('discoverConfig')) { + $provide.decorator('discoverConfig', function ($delegate, userProfile) { + return { + getHideWriteControls() { + if (!userProfile.canWriteSavedObject('search')) { + return true; + } + + return $delegate.getHideWriteControls(); + } + }; + }); + } + + if ($injector.has('chromeConfig')) { + $provide.decorator('chromeConfig', function ($delegate, userProfile) { + return { + shouldHideNavLink(navLink) { + if (!userProfile.canAccessFeature(navLink.id)) { + return true; + } + + return $delegate.shouldHideNavLink(navLink); + } + }; + }); + } + }); 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 f44e11b876676ee..4681e4cf6609656 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 @@ -19,6 +19,9 @@ const buildRequest = (telemetryOptedIn = null, path = '/app/kibana') => { return { path, + getUserProfile: async () => ({ + toJSON: () => ({}) + }), getSavedObjectsClient: () => { return { get, diff --git a/x-pack/plugins/xpack_main/server/lib/feature_registry/register_oss_features.ts b/x-pack/plugins/xpack_main/server/lib/feature_registry/register_oss_features.ts new file mode 100644 index 000000000000000..a64e130253f9f3a --- /dev/null +++ b/x-pack/plugins/xpack_main/server/lib/feature_registry/register_oss_features.ts @@ -0,0 +1,54 @@ +/* + * 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, registerFeature } from './feature_registry'; + +const kibanaFeatures: Feature[] = [ + { + id: 'kibana:discover', + name: 'Discover', + type: 'app', + icon: 'discoverApp', + }, + { + id: 'kibana:visualize', + name: 'Visualize', + type: 'app', + icon: 'visualizeApp', + }, + { + id: 'kibana:dashboard', + name: 'Dashboard', + type: 'app', + icon: 'dashboardApp', + }, + { + id: 'kibana:dev_tools', + name: 'Dev Tools', + type: 'app', + icon: 'devToolsApp', + }, + { + id: 'kibana:management', + name: 'Management', + type: 'app', + icon: 'managementApp', + }, +]; + +const timelionFeatures: Feature[] = [ + { + id: 'timelion', + name: 'Timelion', + type: 'app', + icon: 'timelionApp', + }, +]; + +export function registerOssFeatures() { + kibanaFeatures.forEach(registerFeature); + timelionFeatures.forEach(registerFeature); +} diff --git a/x-pack/plugins/xpack_main/server/lib/user_profile/index.ts b/x-pack/plugins/xpack_main/server/lib/user_profile/index.ts new file mode 100644 index 000000000000000..73880d8c0e81bad --- /dev/null +++ b/x-pack/plugins/xpack_main/server/lib/user_profile/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 { userProfileMixin } from './user_profile_mixin'; +export { registerUserProfileCapabilityDecorator } from './user_profile_capability_registry'; diff --git a/x-pack/plugins/xpack_main/server/lib/user_profile/priority_collection.ts b/x-pack/plugins/xpack_main/server/lib/user_profile/priority_collection.ts new file mode 100644 index 000000000000000..2bb4337f0452549 --- /dev/null +++ b/x-pack/plugins/xpack_main/server/lib/user_profile/priority_collection.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +interface PriorityCollectionEntry { + priority: number; + value: T; +} + +export class PriorityCollection { + private readonly array: Array> = []; + + public add(priority: number, value: T) { + const foundIndex = this.array.findIndex(current => { + if (priority === current.priority) { + throw new Error('Already have entry with this priority'); + } + + return priority < current.priority; + }); + + const spliceIndex = foundIndex === -1 ? this.array.length : foundIndex; + this.array.splice(spliceIndex, 0, { priority, value }); + } + + public toPrioritizedArray(): T[] { + return this.array.map(entry => entry.value); + } +} diff --git a/x-pack/plugins/xpack_main/server/lib/user_profile/user_profile_capability_registry.ts b/x-pack/plugins/xpack_main/server/lib/user_profile/user_profile_capability_registry.ts new file mode 100644 index 000000000000000..ec07ec66d7d7f8b --- /dev/null +++ b/x-pack/plugins/xpack_main/server/lib/user_profile/user_profile_capability_registry.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 { Capabilities } from 'x-pack/plugins/xpack_main/common'; +import { PriorityCollection } from './priority_collection'; + +export type CapabilityDecorator = ( + server: Record, + request: Record, + capabilities: Capabilities +) => Promise; + +const decorators: PriorityCollection = new PriorityCollection(); + +export function registerUserProfileCapabilityDecorator( + priority: number, + decorator: CapabilityDecorator +) { + decorators.add(priority, decorator); +} + +export async function buildUserCapabilities( + server: Record, + request: Record +): Promise { + const decoratedCapabilities = await executeDecorators(server, request, {}); + + return decoratedCapabilities; +} + +async function executeDecorators( + server: Record, + request: Record, + capabilities: Capabilities +): Promise { + return await asyncForEach(decorators.toPrioritizedArray(), server, request, capabilities); +} + +async function asyncForEach( + array: CapabilityDecorator[], + server: Record, + request: Record, + initialCapabilities: Capabilities +) { + let capabilities = initialCapabilities; + + for (const callback of array) { + capabilities = await callback(server, request, capabilities); + } + + return capabilities; +} diff --git a/x-pack/plugins/xpack_main/server/lib/user_profile/user_profile_mixin.ts b/x-pack/plugins/xpack_main/server/lib/user_profile/user_profile_mixin.ts new file mode 100644 index 000000000000000..b0aadb201f470d4 --- /dev/null +++ b/x-pack/plugins/xpack_main/server/lib/user_profile/user_profile_mixin.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 { UserProfile } from '../../../common'; +import { buildUserCapabilities } from './user_profile_capability_registry'; + +export function userProfileMixin(kbnServer: Record, server: Record) { + const profileCache = new WeakMap(); + + server.decorate('request', 'getUserProfile', async function getUserProfile() { + // @ts-ignore + const request: Record = this; + + if (profileCache.has(request)) { + return profileCache.get(request); + } + + const userCapabilities = await buildUserCapabilities(server, request); + const profile = new UserProfile(userCapabilities); + + profileCache.set(request, profile); + + return profile; + }); +}