diff --git a/src/core/public/saved_objects/saved_objects_client.test.ts b/src/core/public/saved_objects/saved_objects_client.test.ts index e633e00965c6ab..0c34a16c68e99d 100644 --- a/src/core/public/saved_objects/saved_objects_client.test.ts +++ b/src/core/public/saved_objects/saved_objects_client.test.ts @@ -448,23 +448,4 @@ describe('SavedObjectsClient', () => { `); }); }); - - it('maintains backwards compatibility by transforming http.fetch errors to be compatible with kfetch errors', () => { - const err = { - response: { ok: false, redirected: false, status: 409, statusText: 'Conflict' }, - body: 'response body', - }; - http.fetch.mockRejectedValue(err); - return expect(savedObjectsClient.get(doc.type, doc.id)).rejects.toMatchInlineSnapshot(` - Object { - "body": "response body", - "res": Object { - "ok": false, - "redirected": false, - "status": 409, - "statusText": "Conflict", - }, - } - `); - }); }); diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts index dab98ee66cdb10..ccb23793a85349 100644 --- a/src/core/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -465,11 +465,7 @@ export class SavedObjectsClient { * uses `{response: {status: number}}`. */ private savedObjectsFetch(path: string, { method, query, body }: HttpFetchOptions) { - return this.http.fetch(path, { method, query, body }).catch(err => { - const kfetchError = Object.assign(err, { res: err.response }); - delete kfetchError.response; - return Promise.reject(kfetchError); - }); + return this.http.fetch(path, { method, query, body }); } } diff --git a/src/plugins/management/public/index.ts b/src/plugins/management/public/index.ts index faec466dbd6714..4ece75bbf36da3 100644 --- a/src/plugins/management/public/index.ts +++ b/src/plugins/management/public/index.ts @@ -24,7 +24,12 @@ export function plugin(initializerContext: PluginInitializerContext) { return new ManagementPlugin(); } -export { ManagementSetup, ManagementStart, RegisterManagementApp } from './types'; +export { + ManagementSetup, + ManagementStart, + RegisterManagementApp, + RegisterManagementAppArgs, +} from './types'; export { ManagementApp } from './management_app'; export { ManagementSection } from './management_section'; export { ManagementSidebarNav } from './components'; // for use in legacy management apps diff --git a/x-pack/legacy/plugins/security/common/model.ts b/x-pack/legacy/plugins/security/common/model.ts deleted file mode 100644 index 733e89f774db8c..00000000000000 --- a/x-pack/legacy/plugins/security/common/model.ts +++ /dev/null @@ -1,28 +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 { - ApiKey, - ApiKeyToInvalidate, - AuthenticatedUser, - BuiltinESPrivileges, - EditUser, - FeaturesPrivileges, - InlineRoleTemplate, - InvalidRoleTemplate, - KibanaPrivileges, - RawKibanaFeaturePrivileges, - RawKibanaPrivileges, - Role, - RoleIndexPrivilege, - RoleKibanaPrivilege, - RoleMapping, - RoleTemplate, - StoredRoleTemplate, - User, - canUserChangePassword, - getUserDisplayName, -} from '../../../../plugins/security/common/model'; diff --git a/x-pack/legacy/plugins/security/index.d.ts b/x-pack/legacy/plugins/security/index.d.ts index 18284c8be689a1..d453415f73376c 100644 --- a/x-pack/legacy/plugins/security/index.d.ts +++ b/x-pack/legacy/plugins/security/index.d.ts @@ -5,7 +5,7 @@ */ import { Legacy } from 'kibana'; -import { AuthenticatedUser } from './common/model'; +import { AuthenticatedUser } from '../../../plugins/security/public'; /** * Public interface of the security plugin. diff --git a/x-pack/legacy/plugins/security/index.js b/x-pack/legacy/plugins/security/index.js index bc403b803b8df0..4988c30a1398b0 100644 --- a/x-pack/legacy/plugins/security/index.js +++ b/x-pack/legacy/plugins/security/index.js @@ -40,8 +40,6 @@ export const security = kibana => }, uiExports: { - chromeNavControls: [], - managementSections: ['plugins/security/views/management'], styleSheetPaths: resolve(__dirname, 'public/index.scss'), apps: [ { @@ -76,7 +74,6 @@ export const security = kibana => 'plugins/security/hacks/on_unauthorized_response', 'plugins/security/hacks/register_account_management_app', ], - home: ['plugins/security/register_feature'], injectDefaultVars: server => { const securityPlugin = server.newPlatform.setup.plugins.security; if (!securityPlugin) { diff --git a/x-pack/legacy/plugins/security/public/documentation_links.js b/x-pack/legacy/plugins/security/public/documentation_links.js deleted file mode 100644 index 8050289b95e9df..00000000000000 --- a/x-pack/legacy/plugins/security/public/documentation_links.js +++ /dev/null @@ -1,16 +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 { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; - -const ES_REF_URL = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}`; - -export const documentationLinks = { - dashboardViewMode: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/xpack-view-modes.html`, - esClusterPrivileges: `${ES_REF_URL}/security-privileges.html#privileges-list-cluster`, - esIndicesPrivileges: `${ES_REF_URL}/security-privileges.html#privileges-list-indices`, - esRunAsPrivileges: `${ES_REF_URL}/security-privileges.html#_run_as_privilege`, -}; diff --git a/x-pack/legacy/plugins/security/public/images/logout.svg b/x-pack/legacy/plugins/security/public/images/logout.svg deleted file mode 100644 index d6533c07199040..00000000000000 --- a/x-pack/legacy/plugins/security/public/images/logout.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/x-pack/legacy/plugins/security/public/images/person.svg b/x-pack/legacy/plugins/security/public/images/person.svg deleted file mode 100644 index 988ddac8859d71..00000000000000 --- a/x-pack/legacy/plugins/security/public/images/person.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/x-pack/legacy/plugins/security/public/index.scss b/x-pack/legacy/plugins/security/public/index.scss index 2d7696bed39890..187ad5231534d2 100644 --- a/x-pack/legacy/plugins/security/public/index.scss +++ b/x-pack/legacy/plugins/security/public/index.scss @@ -15,3 +15,6 @@ $secFormWidth: 460px; // Public views @import './views/index'; +// Styles of Kibana Platform plugin +@import '../../../../plugins/security/public/index'; + diff --git a/x-pack/legacy/plugins/security/public/lib/__tests__/util.js b/x-pack/legacy/plugins/security/public/lib/__tests__/util.js deleted file mode 100644 index 3f7d8aea53a85f..00000000000000 --- a/x-pack/legacy/plugins/security/public/lib/__tests__/util.js +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { toggle, toggleSort } from '../../../public/lib/util'; - -describe('util', () => { - describe('toggle', () => { - it('should add an item to a collection if not already included', () => { - const collection = [1, 2, 3, 4, 5]; - toggle(collection, 6); - expect(collection.indexOf(6)).to.be.above(0); - }); - - it('should remove an item from a collection if already included', () => { - const collection = [1, 2, 3, 4, 5]; - toggle(collection, 3); - expect(collection.indexOf(3)).to.be.below(0); - }); - }); - - describe('toggleSort', () => { - it('should toggle reverse if called with the same orderBy', () => { - const sort = { orderBy: 'foo', reverse: false }; - - toggleSort(sort, 'foo'); - expect(sort.reverse).to.be.true; - - toggleSort(sort, 'foo'); - expect(sort.reverse).to.be.false; - }); - - it('should change orderBy and set reverse to false when called with a different orderBy', () => { - const sort = { orderBy: 'foo', reverse: false }; - - toggleSort(sort, 'bar'); - expect(sort.orderBy).to.equal('bar'); - expect(sort.reverse).to.be.false; - - sort.reverse = true; - toggleSort(sort, 'foo'); - expect(sort.orderBy).to.equal('foo'); - expect(sort.reverse).to.be.false; - }); - }); -}); diff --git a/x-pack/legacy/plugins/security/public/lib/api.ts b/x-pack/legacy/plugins/security/public/lib/api.ts deleted file mode 100644 index c5c6994bf4be36..00000000000000 --- a/x-pack/legacy/plugins/security/public/lib/api.ts +++ /dev/null @@ -1,56 +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 { kfetch } from 'ui/kfetch'; -import { Role, User, EditUser } from '../../common/model'; - -const usersUrl = '/internal/security/users'; -const rolesUrl = '/api/security/role'; - -export class UserAPIClient { - public async getUsers(): Promise { - return await kfetch({ pathname: usersUrl }); - } - - public async getUser(username: string): Promise { - const url = `${usersUrl}/${encodeURIComponent(username)}`; - return await kfetch({ pathname: url }); - } - - public async deleteUser(username: string) { - const url = `${usersUrl}/${encodeURIComponent(username)}`; - await kfetch({ pathname: url, method: 'DELETE' }, {}); - } - - public async saveUser(user: EditUser) { - const url = `${usersUrl}/${encodeURIComponent(user.username)}`; - - await kfetch({ pathname: url, body: JSON.stringify(user), method: 'POST' }); - } - - public async getRoles(): Promise { - return await kfetch({ pathname: rolesUrl }); - } - - public async getRole(name: string): Promise { - const url = `${rolesUrl}/${encodeURIComponent(name)}`; - return await kfetch({ pathname: url }); - } - - public async changePassword(username: string, password: string, currentPassword: string) { - const data: Record = { - newPassword: password, - }; - if (currentPassword) { - data.password = currentPassword; - } - await kfetch({ - pathname: `${usersUrl}/${encodeURIComponent(username)}/password`, - method: 'POST', - body: JSON.stringify(data), - }); - } -} diff --git a/x-pack/legacy/plugins/security/public/lib/api_keys_api.ts b/x-pack/legacy/plugins/security/public/lib/api_keys_api.ts deleted file mode 100644 index fbc0460c5908a7..00000000000000 --- a/x-pack/legacy/plugins/security/public/lib/api_keys_api.ts +++ /dev/null @@ -1,47 +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 { kfetch } from 'ui/kfetch'; -import { ApiKey, ApiKeyToInvalidate } from '../../common/model'; - -interface CheckPrivilegesResponse { - areApiKeysEnabled: boolean; - isAdmin: boolean; -} - -interface InvalidateApiKeysResponse { - itemsInvalidated: ApiKeyToInvalidate[]; - errors: any[]; -} - -interface GetApiKeysResponse { - apiKeys: ApiKey[]; -} - -const apiKeysUrl = `/internal/security/api_key`; - -export class ApiKeysApi { - public static async checkPrivileges(): Promise { - return kfetch({ pathname: `${apiKeysUrl}/privileges` }); - } - - public static async getApiKeys(isAdmin: boolean = false): Promise { - const query = { - isAdmin, - }; - - return kfetch({ pathname: apiKeysUrl, query }); - } - - public static async invalidateApiKeys( - apiKeys: ApiKeyToInvalidate[], - isAdmin: boolean = false - ): Promise { - const pathname = `${apiKeysUrl}/invalidate`; - const body = JSON.stringify({ apiKeys, isAdmin }); - return kfetch({ pathname, method: 'POST', body }); - } -} diff --git a/x-pack/legacy/plugins/security/public/lib/role_utils.ts b/x-pack/legacy/plugins/security/public/lib/role_utils.ts deleted file mode 100644 index c33b7385306fb8..00000000000000 --- a/x-pack/legacy/plugins/security/public/lib/role_utils.ts +++ /dev/null @@ -1,58 +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 { cloneDeep, get } from 'lodash'; -import { Role } from '../../common/model'; - -/** - * Returns whether given role is enabled or not - * - * @param role Object Role JSON, as returned by roles API - * @return Boolean true if role is enabled; false otherwise - */ -export function isRoleEnabled(role: Partial) { - return get(role, 'transient_metadata.enabled', true); -} - -/** - * Returns whether given role is reserved or not. - * - * @param {role} the Role as returned by roles API - */ -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); -} - -/** - * Creates a deep copy of the role suitable for cloning. - * - * @param role the Role to clone. - */ -export function prepareRoleClone(role: Role): Role { - const clone = copyRole(role); - - clone.name = ''; - - return clone; -} diff --git a/x-pack/legacy/plugins/security/public/lib/roles_api.ts b/x-pack/legacy/plugins/security/public/lib/roles_api.ts deleted file mode 100644 index 20c1491ccaac69..00000000000000 --- a/x-pack/legacy/plugins/security/public/lib/roles_api.ts +++ /dev/null @@ -1,25 +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 { kfetch } from 'ui/kfetch'; -import { Role } from '../../common/model'; - -export class RolesApi { - public static async getRoles(): Promise { - return kfetch({ pathname: '/api/security/role' }); - } - - public static async getRole(roleName: string): Promise { - return kfetch({ pathname: `/api/security/role/${encodeURIComponent(roleName)}` }); - } - - public static async deleteRole(roleName: string) { - return kfetch({ - pathname: `/api/security/role/${encodeURIComponent(roleName)}`, - method: 'DELETE', - }); - } -} diff --git a/x-pack/legacy/plugins/security/public/lib/transform_role_for_save.ts b/x-pack/legacy/plugins/security/public/lib/transform_role_for_save.ts deleted file mode 100644 index 861ba530050a13..00000000000000 --- a/x-pack/legacy/plugins/security/public/lib/transform_role_for_save.ts +++ /dev/null @@ -1,41 +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 { 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/legacy/plugins/security/public/lib/util.js b/x-pack/legacy/plugins/security/public/lib/util.js deleted file mode 100644 index bdf44aa3f10bb8..00000000000000 --- a/x-pack/legacy/plugins/security/public/lib/util.js +++ /dev/null @@ -1,19 +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 function toggle(collection, item) { - const i = collection.indexOf(item); - if (i >= 0) collection.splice(i, 1); - else collection.push(item); -} - -export function toggleSort(sort, orderBy) { - if (sort.orderBy === orderBy) sort.reverse = !sort.reverse; - else { - sort.orderBy = orderBy; - sort.reverse = false; - } -} diff --git a/x-pack/legacy/plugins/security/public/objects/lib/get_fields.ts b/x-pack/legacy/plugins/security/public/objects/lib/get_fields.ts deleted file mode 100644 index 91d98782dab423..00000000000000 --- a/x-pack/legacy/plugins/security/public/objects/lib/get_fields.ts +++ /dev/null @@ -1,15 +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 { IHttpResponse } from 'angular'; -import chrome from 'ui/chrome'; - -const apiBase = chrome.addBasePath(`/internal/security/fields`); - -export async function getFields($http: any, query: string): Promise { - return await $http - .get(`${apiBase}/${query}`) - .then((response: IHttpResponse) => response.data || []); -} diff --git a/x-pack/legacy/plugins/security/public/objects/lib/roles.ts b/x-pack/legacy/plugins/security/public/objects/lib/roles.ts deleted file mode 100644 index e33cbe4c6c031c..00000000000000 --- a/x-pack/legacy/plugins/security/public/objects/lib/roles.ts +++ /dev/null @@ -1,21 +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 chrome from 'ui/chrome'; -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, spacesEnabled: boolean) { - const data = transformRoleForSave(copyRole(role), spacesEnabled); - - return await $http.put(`${apiBase}/${role.name}`, data); -} - -export async function deleteRole($http: any, name: string) { - return await $http.delete(`${apiBase}/${name}`); -} diff --git a/x-pack/legacy/plugins/security/public/register_feature.js b/x-pack/legacy/plugins/security/public/register_feature.js deleted file mode 100644 index c0bd42690b6fdb..00000000000000 --- a/x-pack/legacy/plugins/security/public/register_feature.js +++ /dev/null @@ -1,29 +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 { - FeatureCatalogueRegistryProvider, - FeatureCatalogueCategory, -} from 'ui/registry/feature_catalogue'; - -import { i18n } from '@kbn/i18n'; - -FeatureCatalogueRegistryProvider.register(() => { - return { - id: 'security', - title: i18n.translate('xpack.security.registerFeature.securitySettingsTitle', { - defaultMessage: 'Security Settings', - }), - description: i18n.translate('xpack.security.registerFeature.securitySettingsDescription', { - defaultMessage: - 'Protect your data and easily manage who has access to what with users and roles.', - }), - icon: 'securityApp', - path: '/app/kibana#/management/security', - showOnHomePage: true, - category: FeatureCatalogueCategory.ADMIN, - }; -}); diff --git a/x-pack/legacy/plugins/security/public/services/shield_indices.js b/x-pack/legacy/plugins/security/public/services/shield_indices.js deleted file mode 100644 index 791fa6cb596488..00000000000000 --- a/x-pack/legacy/plugins/security/public/services/shield_indices.js +++ /dev/null @@ -1,18 +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 { uiModules } from 'ui/modules'; - -const module = uiModules.get('security', []); -module.service('shieldIndices', ($http, chrome) => { - return { - getFields: query => { - return $http - .get(chrome.addBasePath(`/internal/security/fields/${query}`)) - .then(response => response.data); - }, - }; -}); diff --git a/x-pack/legacy/plugins/security/public/services/shield_role.js b/x-pack/legacy/plugins/security/public/services/shield_role.js deleted file mode 100644 index 261d3449a7a2d1..00000000000000 --- a/x-pack/legacy/plugins/security/public/services/shield_role.js +++ /dev/null @@ -1,30 +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 'angular-resource'; -import { omit } from 'lodash'; -import angular from 'angular'; -import { uiModules } from 'ui/modules'; - -const module = uiModules.get('security', ['ngResource']); -module.service('ShieldRole', ($resource, chrome) => { - return $resource( - chrome.addBasePath('/api/security/role/:name'), - { - name: '@name', - }, - { - save: { - method: 'PUT', - transformRequest(data) { - return angular.toJson( - omit(data, 'name', 'transient_metadata', '_unrecognized_applications') - ); - }, - }, - } - ); -}); diff --git a/x-pack/legacy/plugins/security/public/views/_index.scss b/x-pack/legacy/plugins/security/public/views/_index.scss index b85a7e19973906..6c2a091adf536e 100644 --- a/x-pack/legacy/plugins/security/public/views/_index.scss +++ b/x-pack/legacy/plugins/security/public/views/_index.scss @@ -1,5 +1,2 @@ // Login styles @import './login/index'; - -// Management styles -@import './management/index'; diff --git a/x-pack/legacy/plugins/security/public/views/account/account.html b/x-pack/legacy/plugins/security/public/views/account/account.html deleted file mode 100644 index 0935c415b18295..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/account/account.html +++ /dev/null @@ -1 +0,0 @@ -
diff --git a/x-pack/legacy/plugins/security/public/views/account/account.js b/x-pack/legacy/plugins/security/public/views/account/account.js index 70a7b8dce727ec..13abc44e08f965 100644 --- a/x-pack/legacy/plugins/security/public/views/account/account.js +++ b/x-pack/legacy/plugins/security/public/views/account/account.js @@ -4,17 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import routes from 'ui/routes'; -import template from './account.html'; -import { i18n } from '@kbn/i18n'; -import { I18nContext } from 'ui/i18n'; -import { npSetup } from 'ui/new_platform'; -import { AccountManagementPage } from './components'; import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; +import { i18n } from '@kbn/i18n'; +import { npStart } from 'ui/new_platform'; +import routes from 'ui/routes'; routes.when('/account', { - template, + template: '
', k7Breadcrumbs: () => [ { text: i18n.translate('xpack.security.account.breadcrumb', { @@ -24,19 +21,15 @@ routes.when('/account', { ], controllerAs: 'accountController', controller($scope) { - $scope.$on('$destroy', () => { - const elem = document.getElementById('userProfileReactRoot'); - if (elem) { - unmountComponentAtNode(elem); - } - }); $scope.$$postDigest(() => { + const domNode = document.getElementById('userProfileReactRoot'); + render( - - - , - document.getElementById('userProfileReactRoot') + , + domNode ); + + $scope.$on('$destroy', () => unmountComponentAtNode(domNode)); }); }, }); diff --git a/x-pack/legacy/plugins/security/public/views/login/_index.scss b/x-pack/legacy/plugins/security/public/views/login/_index.scss index 9f133940f79777..9083c8dc3b7751 100644 --- a/x-pack/legacy/plugins/security/public/views/login/_index.scss +++ b/x-pack/legacy/plugins/security/public/views/login/_index.scss @@ -5,5 +5,4 @@ // loginChart__legend--small // loginChart__legend-isLoading -@import 'login'; - +@import './components/index'; diff --git a/x-pack/legacy/plugins/security/public/views/login/components/_index.scss b/x-pack/legacy/plugins/security/public/views/login/components/_index.scss new file mode 100644 index 00000000000000..a6f9598b9cc043 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/login/components/_index.scss @@ -0,0 +1 @@ +@import './login_page/index'; diff --git a/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.test.tsx b/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.test.tsx index 93451453a523ab..3a970d582bdc8d 100644 --- a/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.test.tsx +++ b/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.test.tsx @@ -7,7 +7,7 @@ import { EuiButton, EuiCallOut } from '@elastic/eui'; import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { LoginState } from '../../../../../common/login_state'; +import { LoginState } from '../../login_state'; import { BasicLoginForm } from './basic_login_form'; const createMockHttp = ({ simulateError = false } = {}) => { diff --git a/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx b/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx index e6d3b5b7536b6a..c263381fbdb564 100644 --- a/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx +++ b/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx @@ -9,7 +9,7 @@ import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { ChangeEvent, Component, FormEvent, Fragment, MouseEvent } from 'react'; import ReactMarkdown from 'react-markdown'; import { EuiText } from '@elastic/eui'; -import { LoginState } from '../../../../../common/login_state'; +import { LoginState } from '../../login_state'; interface Props { http: any; diff --git a/x-pack/legacy/plugins/security/public/views/login/components/login_page/_index.scss b/x-pack/legacy/plugins/security/public/views/login/components/login_page/_index.scss new file mode 100644 index 00000000000000..4dd2c0cabfb5e8 --- /dev/null +++ b/x-pack/legacy/plugins/security/public/views/login/components/login_page/_index.scss @@ -0,0 +1 @@ +@import './login_page'; diff --git a/x-pack/legacy/plugins/security/public/views/login/_login.scss b/x-pack/legacy/plugins/security/public/views/login/components/login_page/_login_page.scss similarity index 88% rename from x-pack/legacy/plugins/security/public/views/login/_login.scss rename to x-pack/legacy/plugins/security/public/views/login/components/login_page/_login_page.scss index 607e9e6ec5e3f3..cdfad55ee064af 100644 --- a/x-pack/legacy/plugins/security/public/views/login/_login.scss +++ b/x-pack/legacy/plugins/security/public/views/login/components/login_page/_login_page.scss @@ -1,4 +1,3 @@ - .loginWelcome { @include kibanaFullScreenGraphics; } @@ -16,10 +15,6 @@ margin-bottom: $euiSizeXL; } -.loginWelcome__footerAction { - margin-right: $euiSizeS; -} - .loginWelcome__content { position: relative; margin: auto; diff --git a/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.test.tsx b/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.test.tsx index c16db007bdbdcf..a0318d50a45e58 100644 --- a/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.test.tsx +++ b/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.test.tsx @@ -6,7 +6,7 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { LoginLayout, LoginState } from '../../../../../common/login_state'; +import { LoginLayout, LoginState } from '../../login_state'; import { LoginPage } from './login_page'; const createMockHttp = ({ simulateError = false } = {}) => { diff --git a/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.tsx b/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.tsx index e7e56947ca58f8..8035789a30e9df 100644 --- a/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.tsx +++ b/x-pack/legacy/plugins/security/public/views/login/components/login_page/login_page.tsx @@ -19,7 +19,7 @@ import { EuiTitle, } from '@elastic/eui'; import classNames from 'classnames'; -import { LoginState } from '../../../../../common/login_state'; +import { LoginState } from '../../login_state'; import { BasicLoginForm } from '../basic_login_form'; import { DisabledLoginForm } from '../disabled_login_form'; diff --git a/x-pack/legacy/plugins/security/public/views/login/login.html b/x-pack/legacy/plugins/security/public/views/login/login.html deleted file mode 100644 index 2695fabdd63671..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/login/login.html +++ /dev/null @@ -1 +0,0 @@ -
\ No newline at end of file diff --git a/x-pack/legacy/plugins/security/public/views/login/login.tsx b/x-pack/legacy/plugins/security/public/views/login/login.tsx index d9daf2d1f4d0de..0b89ac553c9a88 100644 --- a/x-pack/legacy/plugins/security/public/views/login/login.tsx +++ b/x-pack/legacy/plugins/security/public/views/login/login.tsx @@ -6,16 +6,14 @@ import { i18n } from '@kbn/i18n'; import { get } from 'lodash'; -import { parseNext } from 'plugins/security/lib/parse_next'; import { LoginPage } from 'plugins/security/views/login/components'; -// @ts-ignore -import template from 'plugins/security/views/login/login.html'; import React from 'react'; import { render } from 'react-dom'; import chrome from 'ui/chrome'; import { I18nContext } from 'ui/i18n'; import { parse } from 'url'; -import { LoginState } from '../../../common/login_state'; +import { parseNext } from './parse_next'; +import { LoginState } from './login_state'; const messageMap = { SESSION_EXPIRED: i18n.translate('xpack.security.login.sessionExpiredDescription', { defaultMessage: 'Your session has timed out. Please log in again.', @@ -31,7 +29,7 @@ interface AnyObject { (chrome as AnyObject) .setVisible(false) - .setRootTemplate(template) + .setRootTemplate('
') .setRootController( 'login', ( diff --git a/x-pack/legacy/plugins/security/common/login_state.ts b/x-pack/legacy/plugins/security/public/views/login/login_state.ts similarity index 100% rename from x-pack/legacy/plugins/security/common/login_state.ts rename to x-pack/legacy/plugins/security/public/views/login/login_state.ts diff --git a/x-pack/legacy/plugins/security/public/lib/__tests__/parse_next.js b/x-pack/legacy/plugins/security/public/views/login/parse_next.test.ts similarity index 80% rename from x-pack/legacy/plugins/security/public/lib/__tests__/parse_next.js rename to x-pack/legacy/plugins/security/public/views/login/parse_next.test.ts index 7516433c77f83d..b5e6c7dca41d8f 100644 --- a/x-pack/legacy/plugins/security/public/lib/__tests__/parse_next.js +++ b/x-pack/legacy/plugins/security/public/views/login/parse_next.test.ts @@ -4,12 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import expect from '@kbn/expect'; -import { parseNext } from '../parse_next'; +import { parseNext } from './parse_next'; describe('parseNext', () => { it('should return a function', () => { - expect(parseNext).to.be.a('function'); + expect(parseNext).toBeInstanceOf(Function); }); describe('with basePath defined', () => { @@ -17,14 +16,14 @@ describe('parseNext', () => { it('should return basePath with a trailing slash when next is not specified', () => { const basePath = '/iqf'; const href = `${basePath}/login`; - expect(parseNext(href, basePath)).to.equal(`${basePath}/`); + expect(parseNext(href, basePath)).toEqual(`${basePath}/`); }); it('should properly handle next without hash', () => { const basePath = '/iqf'; const next = `${basePath}/app/kibana`; const href = `${basePath}/login?next=${next}`; - expect(parseNext(href, basePath)).to.equal(next); + expect(parseNext(href, basePath)).toEqual(next); }); it('should properly handle next with hash', () => { @@ -32,7 +31,7 @@ describe('parseNext', () => { const next = `${basePath}/app/kibana`; const hash = '/discover/New-Saved-Search'; const href = `${basePath}/login?next=${next}#${hash}`; - expect(parseNext(href, basePath)).to.equal(`${next}#${hash}`); + expect(parseNext(href, basePath)).toEqual(`${next}#${hash}`); }); it('should properly decode special characters', () => { @@ -40,7 +39,7 @@ describe('parseNext', () => { const next = `${encodeURIComponent(basePath)}%2Fapp%2Fkibana`; const hash = '/discover/New-Saved-Search'; const href = `${basePath}/login?next=${next}#${hash}`; - expect(parseNext(href, basePath)).to.equal(decodeURIComponent(`${next}#${hash}`)); + expect(parseNext(href, basePath)).toEqual(decodeURIComponent(`${next}#${hash}`)); }); // to help prevent open redirect to a different url @@ -48,7 +47,7 @@ describe('parseNext', () => { const basePath = '/iqf'; const next = `https://example.com${basePath}/app/kibana`; const href = `${basePath}/login?next=${next}`; - expect(parseNext(href, basePath)).to.equal(`${basePath}/`); + expect(parseNext(href, basePath)).toEqual(`${basePath}/`); }); // to help prevent open redirect to a different url by abusing encodings @@ -58,7 +57,7 @@ describe('parseNext', () => { const next = `${encodeURIComponent(baseUrl)}%2Fapp%2Fkibana`; const hash = '/discover/New-Saved-Search'; const href = `${basePath}/login?next=${next}#${hash}`; - expect(parseNext(href, basePath)).to.equal(`${basePath}/`); + expect(parseNext(href, basePath)).toEqual(`${basePath}/`); }); // to help prevent open redirect to a different port @@ -66,7 +65,7 @@ describe('parseNext', () => { const basePath = '/iqf'; const next = `http://localhost:5601${basePath}/app/kibana`; const href = `${basePath}/login?next=${next}`; - expect(parseNext(href, basePath)).to.equal(`${basePath}/`); + expect(parseNext(href, basePath)).toEqual(`${basePath}/`); }); // to help prevent open redirect to a different port by abusing encodings @@ -76,7 +75,7 @@ describe('parseNext', () => { const next = `${encodeURIComponent(baseUrl)}%2Fapp%2Fkibana`; const hash = '/discover/New-Saved-Search'; const href = `${basePath}/login?next=${next}#${hash}`; - expect(parseNext(href, basePath)).to.equal(`${basePath}/`); + expect(parseNext(href, basePath)).toEqual(`${basePath}/`); }); // to help prevent open redirect to a different base path @@ -84,18 +83,18 @@ describe('parseNext', () => { const basePath = '/iqf'; const next = '/notbasepath/app/kibana'; const href = `${basePath}/login?next=${next}`; - expect(parseNext(href, basePath)).to.equal(`${basePath}/`); + expect(parseNext(href, basePath)).toEqual(`${basePath}/`); }); // disallow network-path references it('should return / if next is url without protocol', () => { const nextWithTwoSlashes = '//example.com'; const hrefWithTwoSlashes = `/login?next=${nextWithTwoSlashes}`; - expect(parseNext(hrefWithTwoSlashes)).to.equal('/'); + expect(parseNext(hrefWithTwoSlashes)).toEqual('/'); const nextWithThreeSlashes = '///example.com'; const hrefWithThreeSlashes = `/login?next=${nextWithThreeSlashes}`; - expect(parseNext(hrefWithThreeSlashes)).to.equal('/'); + expect(parseNext(hrefWithThreeSlashes)).toEqual('/'); }); }); @@ -103,34 +102,34 @@ describe('parseNext', () => { // trailing slash is important since it must match the cookie path exactly it('should return / with a trailing slash when next is not specified', () => { const href = '/login'; - expect(parseNext(href)).to.equal('/'); + expect(parseNext(href)).toEqual('/'); }); it('should properly handle next without hash', () => { const next = '/app/kibana'; const href = `/login?next=${next}`; - expect(parseNext(href)).to.equal(next); + expect(parseNext(href)).toEqual(next); }); it('should properly handle next with hash', () => { const next = '/app/kibana'; const hash = '/discover/New-Saved-Search'; const href = `/login?next=${next}#${hash}`; - expect(parseNext(href)).to.equal(`${next}#${hash}`); + expect(parseNext(href)).toEqual(`${next}#${hash}`); }); it('should properly decode special characters', () => { const next = '%2Fapp%2Fkibana'; const hash = '/discover/New-Saved-Search'; const href = `/login?next=${next}#${hash}`; - expect(parseNext(href)).to.equal(decodeURIComponent(`${next}#${hash}`)); + expect(parseNext(href)).toEqual(decodeURIComponent(`${next}#${hash}`)); }); // to help prevent open redirect to a different url it('should return / if next includes a protocol/hostname', () => { const next = 'https://example.com/app/kibana'; const href = `/login?next=${next}`; - expect(parseNext(href)).to.equal('/'); + expect(parseNext(href)).toEqual('/'); }); // to help prevent open redirect to a different url by abusing encodings @@ -139,14 +138,14 @@ describe('parseNext', () => { const next = `${encodeURIComponent(baseUrl)}%2Fapp%2Fkibana`; const hash = '/discover/New-Saved-Search'; const href = `/login?next=${next}#${hash}`; - expect(parseNext(href)).to.equal('/'); + expect(parseNext(href)).toEqual('/'); }); // to help prevent open redirect to a different port it('should return / if next includes a port', () => { const next = 'http://localhost:5601/app/kibana'; const href = `/login?next=${next}`; - expect(parseNext(href)).to.equal('/'); + expect(parseNext(href)).toEqual('/'); }); // to help prevent open redirect to a different port by abusing encodings @@ -155,18 +154,18 @@ describe('parseNext', () => { const next = `${encodeURIComponent(baseUrl)}%2Fapp%2Fkibana`; const hash = '/discover/New-Saved-Search'; const href = `/login?next=${next}#${hash}`; - expect(parseNext(href)).to.equal('/'); + expect(parseNext(href)).toEqual('/'); }); // disallow network-path references it('should return / if next is url without protocol', () => { const nextWithTwoSlashes = '//example.com'; const hrefWithTwoSlashes = `/login?next=${nextWithTwoSlashes}`; - expect(parseNext(hrefWithTwoSlashes)).to.equal('/'); + expect(parseNext(hrefWithTwoSlashes)).toEqual('/'); const nextWithThreeSlashes = '///example.com'; const hrefWithThreeSlashes = `/login?next=${nextWithThreeSlashes}`; - expect(parseNext(hrefWithThreeSlashes)).to.equal('/'); + expect(parseNext(hrefWithThreeSlashes)).toEqual('/'); }); }); }); diff --git a/x-pack/legacy/plugins/security/public/lib/parse_next.ts b/x-pack/legacy/plugins/security/public/views/login/parse_next.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/lib/parse_next.ts rename to x-pack/legacy/plugins/security/public/views/login/parse_next.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/_index.scss b/x-pack/legacy/plugins/security/public/views/management/_index.scss deleted file mode 100644 index 78b53845071e4a..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/_index.scss +++ /dev/null @@ -1,4 +0,0 @@ -@import './change_password_form/index'; -@import './edit_role/index'; -@import './edit_user/index'; -@import './role_mappings/edit_role_mapping/index'; \ No newline at end of file diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/api_keys.html b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/api_keys.html deleted file mode 100644 index e46c6f72b5d20e..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/api_keys.html +++ /dev/null @@ -1,3 +0,0 @@ - -
- diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/api_keys.js b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/api_keys.js deleted file mode 100644 index e7143b10208148..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/api_keys.js +++ /dev/null @@ -1,35 +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 React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import routes from 'ui/routes'; -import template from './api_keys.html'; -import { API_KEYS_PATH } from '../management_urls'; -import { getApiKeysBreadcrumbs } from '../breadcrumbs'; -import { I18nContext } from 'ui/i18n'; -import { ApiKeysGridPage } from './components'; - -routes.when(API_KEYS_PATH, { - template, - k7Breadcrumbs: getApiKeysBreadcrumbs, - controller($scope) { - $scope.$$postDigest(() => { - const domNode = document.getElementById('apiKeysGridReactRoot'); - - render( - - - , - domNode - ); - - // unmount react on controller destroy - $scope.$on('$destroy', () => { - unmountComponentAtNode(domNode); - }); - }); - }, -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/breadcrumbs.ts b/x-pack/legacy/plugins/security/public/views/management/breadcrumbs.ts deleted file mode 100644 index 4ab7e45e848498..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/breadcrumbs.ts +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import { MANAGEMENT_BREADCRUMB } from 'ui/management/breadcrumbs'; - -export function getUsersBreadcrumbs() { - return [ - MANAGEMENT_BREADCRUMB, - { - text: i18n.translate('xpack.security.users.breadcrumb', { - defaultMessage: 'Users', - }), - href: '#/management/security/users', - }, - ]; -} - -export function getEditUserBreadcrumbs($route: Record) { - const { username } = $route.current.params; - return [ - ...getUsersBreadcrumbs(), - { - text: username, - href: `#/management/security/users/edit/${username}`, - }, - ]; -} - -export function getCreateUserBreadcrumbs() { - return [ - ...getUsersBreadcrumbs(), - { - text: i18n.translate('xpack.security.users.createBreadcrumb', { - defaultMessage: 'Create', - }), - }, - ]; -} - -export function getRolesBreadcrumbs() { - return [ - MANAGEMENT_BREADCRUMB, - { - text: i18n.translate('xpack.security.roles.breadcrumb', { - defaultMessage: 'Roles', - }), - href: '#/management/security/roles', - }, - ]; -} - -export function getEditRoleBreadcrumbs($route: Record) { - const { name } = $route.current.params; - return [ - ...getRolesBreadcrumbs(), - { - text: name, - href: `#/management/security/roles/edit/${name}`, - }, - ]; -} - -export function getCreateRoleBreadcrumbs() { - return [ - ...getUsersBreadcrumbs(), - { - text: i18n.translate('xpack.security.roles.createBreadcrumb', { - defaultMessage: 'Create', - }), - }, - ]; -} - -export function getApiKeysBreadcrumbs() { - return [ - MANAGEMENT_BREADCRUMB, - { - text: i18n.translate('xpack.security.apiKeys.breadcrumb', { - defaultMessage: 'API Keys', - }), - href: '#/management/security/api_keys', - }, - ]; -} - -export function getRoleMappingBreadcrumbs() { - return [ - MANAGEMENT_BREADCRUMB, - { - text: i18n.translate('xpack.security.roleMapping.breadcrumb', { - defaultMessage: 'Role Mappings', - }), - href: '#/management/security/role_mappings', - }, - ]; -} - -export function getEditRoleMappingBreadcrumbs($route: Record) { - const { name } = $route.current.params; - return [ - ...getRoleMappingBreadcrumbs(), - { - text: - name || - i18n.translate('xpack.security.roleMappings.createBreadcrumb', { - defaultMessage: 'Create', - }), - href: `#/management/security/role_mappings/edit/${name}`, - }, - ]; -} diff --git a/x-pack/legacy/plugins/security/public/views/management/change_password_form/_change_password_form.scss b/x-pack/legacy/plugins/security/public/views/management/change_password_form/_change_password_form.scss deleted file mode 100644 index 98331c2070a31e..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/change_password_form/_change_password_form.scss +++ /dev/null @@ -1,17 +0,0 @@ -.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/legacy/plugins/security/public/views/management/change_password_form/_index.scss b/x-pack/legacy/plugins/security/public/views/management/change_password_form/_index.scss deleted file mode 100644 index a6058b5ddebbf0..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/change_password_form/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './change_password_form'; diff --git a/x-pack/legacy/plugins/security/public/views/management/change_password_form/change_password_form.html b/x-pack/legacy/plugins/security/public/views/management/change_password_form/change_password_form.html deleted file mode 100644 index 92fb95861a6f8e..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/change_password_form/change_password_form.html +++ /dev/null @@ -1,141 +0,0 @@ - - -
- - - - - - -
- -
- - - - -
-
- - -
- - - - -
-
- - -
- - - - -
- - -
- - -
-
- - - -
-
-
diff --git a/x-pack/legacy/plugins/security/public/views/management/change_password_form/change_password_form.js b/x-pack/legacy/plugins/security/public/views/management/change_password_form/change_password_form.js deleted file mode 100644 index d9aa59f6df1427..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/change_password_form/change_password_form.js +++ /dev/null @@ -1,48 +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 { uiModules } from 'ui/modules'; -import template from './change_password_form.html'; - -const module = uiModules.get('security', ['kibana']); -module.directive('kbnChangePasswordForm', function() { - return { - template, - scope: { - requireCurrentPassword: '=', - showKibanaWarning: '=', - onChangePassword: '&', - }, - restrict: 'E', - replace: true, - controllerAs: 'changePasswordController', - controller: function($scope) { - this.currentPassword = null; - this.newPassword = null; - this.newPasswordConfirmation = null; - this.isFormVisible = false; - this.isIncorrectPassword = false; - - this.showForm = () => { - this.isFormVisible = true; - }; - - this.hideForm = () => { - $scope.changePasswordForm.$setPristine(); - $scope.changePasswordForm.$setUntouched(); - this.currentPassword = null; - this.newPassword = null; - this.newPasswordConfirmation = null; - this.isFormVisible = false; - this.isIncorrectPassword = false; - }; - - this.onIncorrectPassword = () => { - this.isIncorrectPassword = true; - }; - }, - }; -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/_index.scss b/x-pack/legacy/plugins/security/public/views/management/edit_role/_index.scss deleted file mode 100644 index 192091fb04e3c9..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './components/index'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/_index.scss b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/_index.scss deleted file mode 100644 index 32b3832e7a9fa9..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/_index.scss +++ /dev/null @@ -1,9 +0,0 @@ -@import './collapsible_panel/collapsible_panel'; -@import './privileges/kibana/space_aware_privilege_section/index'; -@import './privileges/kibana/feature_table/index'; -@import './spaces_popover_list/spaces_popover_list'; - -.secPrivilegeFeatureIcon { - flex-shrink: 0; - margin-right: $euiSizeS; -} diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.test.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.test.tsx deleted file mode 100644 index 67c32c8393171f..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.test.tsx +++ /dev/null @@ -1,716 +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 { ReactWrapper } from 'enzyme'; -import React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { UICapabilities } from 'ui/capabilities'; -import { Space } from '../../../../../../spaces/common/model/space'; -import { Feature } from '../../../../../../../../plugins/features/public'; -// These modules should be moved into a common directory -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { Actions } from '../../../../../../../../plugins/security/server/authorization/actions'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { privilegesFactory } from '../../../../../../../../plugins/security/server/authorization/privileges'; -import { RawKibanaPrivileges, Role } from '../../../../../common/model'; -import { EditRolePage } from './edit_role_page'; -import { SimplePrivilegeSection } from './privileges/kibana/simple_privilege_section'; -import { SpaceAwarePrivilegeSection } from './privileges/kibana/space_aware_privilege_section'; -import { TransformErrorSection } from './privileges/kibana/transform_error_section'; - -const buildFeatures = () => { - return [ - { - id: 'feature1', - name: 'Feature 1', - icon: 'addDataApp', - app: ['feature1App'], - privileges: { - all: { - app: ['feature1App'], - ui: ['feature1-ui'], - savedObject: { - all: [], - read: [], - }, - }, - }, - }, - { - id: 'feature2', - name: 'Feature 2', - icon: 'addDataApp', - app: ['feature2App'], - privileges: { - all: { - app: ['feature2App'], - ui: ['feature2-ui'], - savedObject: { - all: ['feature2'], - read: ['config'], - }, - }, - }, - }, - ] as Feature[]; -}; - -const buildRawKibanaPrivileges = () => { - return privilegesFactory(new Actions('unit_test_version'), { - getFeatures: () => buildFeatures(), - }).get(); -}; - -const buildBuiltinESPrivileges = () => { - return { - cluster: ['all', 'manage', 'monitor'], - index: ['all', 'read', 'write', 'index'], - }; -}; - -const buildUICapabilities = (canManageSpaces = true) => { - return { - catalogue: {}, - management: {}, - navLinks: {}, - spaces: { - manage: canManageSpaces, - }, - } as UICapabilities; -}; - -const buildSpaces = () => { - return [ - { - id: 'default', - name: 'Default', - disabledFeatures: [], - _reserved: true, - }, - { - id: 'space_1', - name: 'Space 1', - disabledFeatures: [], - }, - { - id: 'space_2', - name: 'Space 2', - disabledFeatures: ['feature2'], - }, - ] as Space[]; -}; - -const expectReadOnlyFormButtons = (wrapper: ReactWrapper) => { - expect(wrapper.find('button[data-test-subj="roleFormReturnButton"]')).toHaveLength(1); - expect(wrapper.find('button[data-test-subj="roleFormSaveButton"]')).toHaveLength(0); -}; - -const expectSaveFormButtons = (wrapper: ReactWrapper) => { - expect(wrapper.find('button[data-test-subj="roleFormReturnButton"]')).toHaveLength(0); - expect(wrapper.find('button[data-test-subj="roleFormSaveButton"]')).toHaveLength(1); -}; - -describe('', () => { - describe('with spaces enabled', () => { - it('can render a reserved role', () => { - const role: Role = { - name: 'superuser', - metadata: { - _reserved: true, - }, - elasticsearch: { - cluster: ['all'], - indices: [], - run_as: ['*'], - }, - kibana: [ - { - spaces: ['*'], - base: ['all'], - feature: {}, - }, - ], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const spaces: Space[] = buildSpaces(); - const uiCapabilities: UICapabilities = buildUICapabilities(); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(1); - expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); - expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); - expectReadOnlyFormButtons(wrapper); - }); - - it('can render a user defined role', () => { - const role: Role = { - name: 'my custom role', - metadata: {}, - elasticsearch: { - cluster: ['all'], - indices: [], - run_as: ['*'], - }, - kibana: [ - { - spaces: ['*'], - base: ['all'], - feature: {}, - }, - ], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const spaces: Space[] = buildSpaces(); - const uiCapabilities: UICapabilities = buildUICapabilities(); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(0); - expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); - expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); - expectSaveFormButtons(wrapper); - }); - - it('can render when creating a new role', () => { - // @ts-ignore - const role: Role = { - metadata: {}, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const spaces: Space[] = buildSpaces(); - const uiCapabilities: UICapabilities = buildUICapabilities(); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); - expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); - expectSaveFormButtons(wrapper); - }); - - it('can render when cloning an existing role', () => { - const role: Role = { - metadata: { - _reserved: false, - }, - name: '', - elasticsearch: { - cluster: ['all', 'manage'], - indices: [ - { - names: ['foo*'], - privileges: ['all'], - field_security: { - except: ['f'], - grant: ['b*'], - }, - }, - ], - run_as: ['elastic'], - }, - kibana: [ - { - spaces: ['*'], - base: ['all'], - feature: {}, - }, - ], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const spaces: Space[] = buildSpaces(); - const uiCapabilities: UICapabilities = buildUICapabilities(); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); - expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); - expectSaveFormButtons(wrapper); - }); - - it('renders an auth error when not authorized to manage spaces', () => { - const role: Role = { - name: 'my custom role', - metadata: {}, - elasticsearch: { - cluster: ['all'], - indices: [], - run_as: ['*'], - }, - kibana: [ - { - spaces: ['*'], - base: ['all'], - feature: {}, - }, - ], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const spaces: Space[] = buildSpaces(); - const uiCapabilities: UICapabilities = buildUICapabilities(false); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(0); - - expect( - wrapper.find('EuiCallOut[data-test-subj="userCannotManageSpacesCallout"]') - ).toHaveLength(1); - - expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); - expectSaveFormButtons(wrapper); - }); - - it('renders a partial read-only view when there is a transform error', () => { - const role: Role = { - name: 'my custom role', - metadata: {}, - elasticsearch: { - cluster: ['all'], - indices: [], - run_as: ['*'], - }, - kibana: [], - _transform_error: ['kibana'], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const spaces: Space[] = buildSpaces(); - const uiCapabilities: UICapabilities = buildUICapabilities(false); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find(TransformErrorSection)).toHaveLength(1); - expectReadOnlyFormButtons(wrapper); - }); - }); - - describe('with spaces disabled', () => { - it('can render a reserved role', () => { - const role: Role = { - name: 'superuser', - metadata: { - _reserved: true, - }, - elasticsearch: { - cluster: ['all'], - indices: [], - run_as: ['*'], - }, - kibana: [ - { - spaces: ['*'], - base: ['all'], - feature: {}, - }, - ], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const uiCapabilities: UICapabilities = buildUICapabilities(); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(1); - expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); - expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); - expectReadOnlyFormButtons(wrapper); - }); - - it('can render a user defined role', () => { - const role: Role = { - name: 'my custom role', - metadata: {}, - elasticsearch: { - cluster: ['all'], - indices: [], - run_as: ['*'], - }, - kibana: [ - { - spaces: ['*'], - base: ['all'], - feature: {}, - }, - ], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const uiCapabilities: UICapabilities = buildUICapabilities(); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(0); - expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); - expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); - expectSaveFormButtons(wrapper); - }); - - it('can render when creating a new role', () => { - // @ts-ignore - const role: Role = { - metadata: {}, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const uiCapabilities: UICapabilities = buildUICapabilities(); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); - expectSaveFormButtons(wrapper); - }); - - it('can render when cloning an existing role', () => { - const role: Role = { - metadata: { - _reserved: false, - }, - name: '', - elasticsearch: { - cluster: ['all', 'manage'], - indices: [ - { - names: ['foo*'], - privileges: ['all'], - field_security: { - except: ['f'], - grant: ['b*'], - }, - }, - ], - run_as: ['elastic'], - }, - kibana: [ - { - spaces: ['*'], - base: ['all'], - feature: {}, - }, - ], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const uiCapabilities: UICapabilities = buildUICapabilities(); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); - expectSaveFormButtons(wrapper); - }); - - it('does not care if user cannot manage spaces', () => { - const role: Role = { - name: 'my custom role', - metadata: {}, - elasticsearch: { - cluster: ['all'], - indices: [], - run_as: ['*'], - }, - kibana: [ - { - spaces: ['*'], - base: ['all'], - feature: {}, - }, - ], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const uiCapabilities: UICapabilities = buildUICapabilities(false); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(0); - - expect( - wrapper.find('EuiCallOut[data-test-subj="userCannotManageSpacesCallout"]') - ).toHaveLength(0); - - expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); - expectSaveFormButtons(wrapper); - }); - - it('renders a partial read-only view when there is a transform error', () => { - const role: Role = { - name: 'my custom role', - metadata: {}, - elasticsearch: { - cluster: ['all'], - indices: [], - run_as: ['*'], - }, - kibana: [], - _transform_error: ['kibana'], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const uiCapabilities: UICapabilities = buildUICapabilities(false); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find(TransformErrorSection)).toHaveLength(1); - expectReadOnlyFormButtons(wrapper); - }); - }); -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.tsx deleted file mode 100644 index 2ba012afa689dc..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.tsx +++ /dev/null @@ -1,409 +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 { - EuiButton, - EuiButtonEmpty, - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiForm, - EuiFormRow, - EuiPanel, - EuiSpacer, - EuiText, - EuiTitle, -} from '@elastic/eui'; -import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import { get } from 'lodash'; -import React, { ChangeEvent, Component, Fragment, HTMLProps } from 'react'; -import { UICapabilities } from 'ui/capabilities'; -import { toastNotifications } from 'ui/notify'; -import { Space } from '../../../../../../spaces/common/model/space'; -import { Feature } from '../../../../../../../../plugins/features/public'; -import { - KibanaPrivileges, - RawKibanaPrivileges, - Role, - BuiltinESPrivileges, -} from '../../../../../common/model'; -import { - isReadOnlyRole, - isReservedRole, - copyRole, - prepareRoleClone, -} from '../../../../lib/role_utils'; -import { deleteRole, saveRole } from '../../../../objects'; -import { ROLES_PATH } from '../../management_urls'; -import { RoleValidationResult, RoleValidator } from '../lib/validate_role'; -import { DeleteRoleButton } from './delete_role_button'; -import { ElasticsearchPrivileges, KibanaPrivilegesRegion } from './privileges'; -import { ReservedRoleBadge } from './reserved_role_badge'; - -interface Props { - action: 'edit' | 'clone'; - role: Role; - runAsUsers: string[]; - indexPatterns: string[]; - httpClient: any; - allowDocumentLevelSecurity: boolean; - allowFieldLevelSecurity: boolean; - kibanaPrivileges: RawKibanaPrivileges; - builtinESPrivileges: BuiltinESPrivileges; - spaces?: Space[]; - spacesEnabled: boolean; - intl: InjectedIntl; - uiCapabilities: UICapabilities; - features: Feature[]; -} - -interface State { - role: Role; - formError: RoleValidationResult | null; -} - -class EditRolePageUI extends Component { - private validator: RoleValidator; - - constructor(props: Props) { - super(props); - - this.validator = new RoleValidator({ shouldValidate: false }); - - let role: Role; - if (props.action === 'clone') { - role = prepareRoleClone(props.role); - } else { - role = copyRole(props.role); - } - - this.state = { - role, - formError: null, - }; - } - - public componentDidMount() { - if (this.props.action === 'clone' && isReservedRole(this.props.role)) { - this.backToRoleList(); - } - } - - public render() { - const description = this.props.spacesEnabled ? ( - - ) : ( - - ); - - return ( -
- - {this.getFormTitle()} - - - - {description} - - {isReservedRole(this.state.role) && ( - - - -

- -

-
-
- )} - - - - {this.getRoleName()} - - {this.getElasticsearchPrivileges()} - - {this.getKibanaPrivileges()} - - - - {this.getFormButtons()} -
-
- ); - } - - private getFormTitle = () => { - let titleText; - const props: HTMLProps = { - tabIndex: 0, - }; - if (isReservedRole(this.state.role)) { - titleText = ( - - ); - props['aria-describedby'] = 'reservedRoleDescription'; - } else if (this.editingExistingRole()) { - titleText = ( - - ); - } else { - titleText = ( - - ); - } - - return ( - -

- {titleText} -

-
- ); - }; - - private getActionButton = () => { - if (this.editingExistingRole() && !isReadOnlyRole(this.state.role)) { - return ( - - - - ); - } - - return null; - }; - - private getRoleName = () => { - return ( - - - } - helpText={ - !isReservedRole(this.state.role) && this.editingExistingRole() ? ( - - ) : ( - undefined - ) - } - {...this.validator.validateRoleName(this.state.role)} - > - - - - ); - }; - - private onNameChange = (e: ChangeEvent) => { - const rawValue = e.target.value; - const name = rawValue.replace(/\s/g, '_'); - - this.setState({ - role: { - ...this.state.role, - name, - }, - }); - }; - - private getElasticsearchPrivileges() { - return ( -
- - -
- ); - } - - private onRoleChange = (role: Role) => { - this.setState({ - role, - }); - }; - - private getKibanaPrivileges = () => { - return ( -
- - -
- ); - }; - - private getFormButtons = () => { - if (isReadOnlyRole(this.state.role)) { - return this.getReturnToRoleListButton(); - } - - return ( - - {this.getSaveButton()} - {this.getCancelButton()} - - {this.getActionButton()} - - ); - }; - - private getReturnToRoleListButton = () => { - return ( - - - - ); - }; - - private getSaveButton = () => { - const saveText = this.editingExistingRole() ? ( - - ) : ( - - ); - - return ( - - {saveText} - - ); - }; - - private getCancelButton = () => { - return ( - - - - ); - }; - - private editingExistingRole = () => { - return !!this.props.role.name && this.props.action === 'edit'; - }; - - private saveRole = () => { - this.validator.enableValidation(); - - const result = this.validator.validateForSave(this.state.role); - if (result.isInvalid) { - this.setState({ - formError: result, - }); - } else { - this.setState({ - formError: null, - }); - - const { httpClient, intl, spacesEnabled } = this.props; - - saveRole(httpClient, this.state.role, spacesEnabled) - .then(() => { - toastNotifications.addSuccess( - intl.formatMessage({ - id: 'xpack.security.management.editRole.roleSuccessfullySavedNotificationMessage', - defaultMessage: 'Saved role', - }) - ); - this.backToRoleList(); - }) - .catch((error: any) => { - toastNotifications.addDanger(get(error, 'data.message')); - }); - } - }; - - private handleDeleteRole = () => { - const { httpClient, role, intl } = this.props; - - deleteRole(httpClient, role.name) - .then(() => { - toastNotifications.addSuccess( - intl.formatMessage({ - id: 'xpack.security.management.editRole.roleSuccessfullyDeletedNotificationMessage', - defaultMessage: 'Deleted role', - }) - ); - this.backToRoleList(); - }) - .catch((error: any) => { - toastNotifications.addDanger(get(error, 'data.message')); - }); - }; - - private backToRoleList = () => { - window.location.hash = ROLES_PATH; - }; -} - -export const EditRolePage = injectI18n(EditRolePageUI); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/elasticsearch_privileges.test.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/elasticsearch_privileges.test.tsx deleted file mode 100644 index 5ba3d1daf61acb..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/elasticsearch_privileges.test.tsx +++ /dev/null @@ -1,96 +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 React from 'react'; -import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { RoleValidator } from '../../../lib/validate_role'; -import { ClusterPrivileges } from './cluster_privileges'; -import { ElasticsearchPrivileges } from './elasticsearch_privileges'; -import { IndexPrivileges } from './index_privileges'; - -test('it renders without crashing', () => { - const props = { - role: { - name: '', - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - }, - editable: true, - httpClient: jest.fn(), - onChange: jest.fn(), - runAsUsers: [], - indexPatterns: [], - allowDocumentLevelSecurity: true, - allowFieldLevelSecurity: true, - validator: new RoleValidator(), - builtinESPrivileges: { - cluster: ['all', 'manage', 'monitor'], - index: ['all', 'read', 'write', 'index'], - }, - }; - const wrapper = shallowWithIntl(); - expect(wrapper).toMatchSnapshot(); -}); - -test('it renders ClusterPrivileges', () => { - const props = { - role: { - name: '', - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - }, - editable: true, - httpClient: jest.fn(), - onChange: jest.fn(), - runAsUsers: [], - indexPatterns: [], - allowDocumentLevelSecurity: true, - allowFieldLevelSecurity: true, - validator: new RoleValidator(), - builtinESPrivileges: { - cluster: ['all', 'manage', 'monitor'], - index: ['all', 'read', 'write', 'index'], - }, - }; - const wrapper = mountWithIntl(); - expect(wrapper.find(ClusterPrivileges)).toHaveLength(1); -}); - -test('it renders IndexPrivileges', () => { - const props = { - role: { - name: '', - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - }, - editable: true, - httpClient: jest.fn(), - onChange: jest.fn(), - runAsUsers: [], - indexPatterns: [], - allowDocumentLevelSecurity: true, - allowFieldLevelSecurity: true, - validator: new RoleValidator(), - builtinESPrivileges: { - cluster: ['all', 'manage', 'monitor'], - index: ['all', 'read', 'write', 'index'], - }, - }; - const wrapper = mountWithIntl(); - expect(wrapper.find(IndexPrivileges)).toHaveLength(1); -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/edit_role.html b/x-pack/legacy/plugins/security/public/views/management/edit_role/edit_role.html deleted file mode 100644 index ca4073dcad6f5c..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/edit_role.html +++ /dev/null @@ -1,3 +0,0 @@ - -
- diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/index.js b/x-pack/legacy/plugins/security/public/views/management/edit_role/index.js deleted file mode 100644 index 27c9beb4ba8284..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/index.js +++ /dev/null @@ -1,176 +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 _ from 'lodash'; -import routes from 'ui/routes'; -import { capabilities } from 'ui/capabilities'; -import { kfetch } from 'ui/kfetch'; -import { fatalError, toastNotifications } from 'ui/notify'; -import { npStart } from 'ui/new_platform'; -import template from 'plugins/security/views/management/edit_role/edit_role.html'; -import 'plugins/security/services/shield_role'; -import 'plugins/security/services/shield_indices'; -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; -import { UserAPIClient } from '../../../lib/api'; -import { ROLES_PATH, CLONE_ROLES_PATH, EDIT_ROLES_PATH } from '../management_urls'; -import { getEditRoleBreadcrumbs, getCreateRoleBreadcrumbs } from '../breadcrumbs'; - -import { EditRolePage } from './components'; - -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { I18nContext } from 'ui/i18n'; -import { i18n } from '@kbn/i18n'; - -const routeDefinition = action => ({ - template, - k7Breadcrumbs: ($injector, $route) => - $injector.invoke( - action === 'edit' && $route.current.params.name - ? getEditRoleBreadcrumbs - : getCreateRoleBreadcrumbs - ), - resolve: { - role($route, ShieldRole, Promise, kbnUrl) { - const name = $route.current.params.name; - - let role; - - if (name != null) { - role = ShieldRole.get({ name }).$promise.catch(response => { - if (response.status === 404) { - toastNotifications.addDanger({ - title: i18n.translate('xpack.security.management.roles.roleNotFound', { - defaultMessage: 'No "{roleName}" role found.', - values: { roleName: name }, - }), - }); - kbnUrl.redirect(ROLES_PATH); - } else { - return fatalError(response); - } - }); - } else { - role = Promise.resolve( - new ShieldRole({ - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - _unrecognized_applications: [], - }) - ); - } - - return role.then(res => res.toJSON()); - }, - users() { - return new UserAPIClient().getUsers().then(users => _.map(users, 'username')); - }, - indexPatterns() { - return npStart.plugins.data.indexPatterns.getTitles(); - }, - spaces(spacesEnabled) { - if (spacesEnabled) { - return kfetch({ method: 'get', pathname: '/api/spaces/space' }); - } - return []; - }, - kibanaPrivileges() { - return kfetch({ - method: 'get', - pathname: '/api/security/privileges', - query: { includeActions: true }, - }); - }, - builtinESPrivileges() { - return kfetch({ method: 'get', pathname: '/internal/security/esPrivileges/builtin' }); - }, - features() { - return kfetch({ method: 'get', pathname: '/api/features' }).catch(e => { - // TODO: This check can be removed once all of these `resolve` entries are moved out of Angular and into the React app. - const unauthorizedForFeatures = _.get(e, 'body.statusCode') === 404; - if (unauthorizedForFeatures) { - return []; - } - throw e; - }); - }, - }, - controllerAs: 'editRole', - controller($injector, $scope, $http, enableSpaceAwarePrivileges) { - const $route = $injector.get('$route'); - const role = $route.current.locals.role; - - const allowDocumentLevelSecurity = xpackInfo.get( - 'features.security.allowRoleDocumentLevelSecurity' - ); - const allowFieldLevelSecurity = xpackInfo.get('features.security.allowRoleFieldLevelSecurity'); - if (role.elasticsearch.indices.length === 0) { - const emptyOption = { - names: [], - privileges: [], - }; - - if (allowFieldLevelSecurity) { - emptyOption.field_security = { - grant: ['*'], - except: [], - }; - } - - if (allowDocumentLevelSecurity) { - emptyOption.query = ''; - } - - role.elasticsearch.indices.push(emptyOption); - } - - const { - users, - indexPatterns, - spaces, - kibanaPrivileges, - builtinESPrivileges, - features, - } = $route.current.locals; - - $scope.$$postDigest(async () => { - const domNode = document.getElementById('editRoleReactRoot'); - - render( - - - , - domNode - ); - - // unmount react on controller destroy - $scope.$on('$destroy', () => { - unmountComponentAtNode(domNode); - }); - }); - }, -}); - -routes.when(`${CLONE_ROLES_PATH}/:name`, routeDefinition('clone')); -routes.when(`${EDIT_ROLES_PATH}/:name?`, routeDefinition('edit')); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_user/_index.scss b/x-pack/legacy/plugins/security/public/views/management/edit_user/_index.scss deleted file mode 100644 index c5da74aa3f785c..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/edit_user/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './users'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_user/edit_user.html b/x-pack/legacy/plugins/security/public/views/management/edit_user/edit_user.html deleted file mode 100644 index 4fa2768480874a..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/edit_user/edit_user.html +++ /dev/null @@ -1,3 +0,0 @@ - -
- diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_user/edit_user.js b/x-pack/legacy/plugins/security/public/views/management/edit_user/edit_user.js deleted file mode 100644 index ab218022c6ee64..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/edit_user/edit_user.js +++ /dev/null @@ -1,58 +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 routes from 'ui/routes'; -import template from 'plugins/security/views/management/edit_user/edit_user.html'; -import 'angular-resource'; -import 'ui/angular_ui_select'; -import 'plugins/security/services/shield_role'; -import { EDIT_USERS_PATH } from '../management_urls'; -import { EditUserPage } from './components'; -import { UserAPIClient } from '../../../lib/api'; -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { I18nContext } from 'ui/i18n'; -import { npSetup } from 'ui/new_platform'; -import { getEditUserBreadcrumbs, getCreateUserBreadcrumbs } from '../breadcrumbs'; - -const renderReact = (elem, changeUrl, username) => { - render( - - - , - elem - ); -}; - -routes.when(`${EDIT_USERS_PATH}/:username?`, { - template, - k7Breadcrumbs: ($injector, $route) => - $injector.invoke( - $route.current.params.username ? getEditUserBreadcrumbs : getCreateUserBreadcrumbs - ), - controllerAs: 'editUser', - controller($scope, $route, kbnUrl) { - $scope.$on('$destroy', () => { - const elem = document.getElementById('editUserReactRoot'); - if (elem) { - unmountComponentAtNode(elem); - } - }); - $scope.$$postDigest(() => { - const elem = document.getElementById('editUserReactRoot'); - const username = $route.current.params.username; - const changeUrl = url => { - kbnUrl.change(url); - $scope.$apply(); - }; - renderReact(elem, changeUrl, username); - }); - }, -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/management.js b/x-pack/legacy/plugins/security/public/views/management/management.js deleted file mode 100644 index f0369f232aeba8..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/management.js +++ /dev/null @@ -1,134 +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 'plugins/security/views/management/change_password_form/change_password_form'; -import 'plugins/security/views/management/password_form/password_form'; -import 'plugins/security/views/management/users_grid/users'; -import 'plugins/security/views/management/roles_grid/roles'; -import 'plugins/security/views/management/api_keys_grid/api_keys'; -import 'plugins/security/views/management/edit_user/edit_user'; -import 'plugins/security/views/management/edit_role/index'; -import 'plugins/security/views/management/role_mappings/role_mappings_grid'; -import 'plugins/security/views/management/role_mappings/edit_role_mapping'; -import routes from 'ui/routes'; -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; -import { ROLES_PATH, USERS_PATH, API_KEYS_PATH, ROLE_MAPPINGS_PATH } from './management_urls'; - -import { management } from 'ui/management'; -import { npSetup } from 'ui/new_platform'; -import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; - -routes - .defaults(/^\/management\/security(\/|$)/, { - resolve: { - showLinks(kbnUrl, Promise) { - if (!xpackInfo.get('features.security.showLinks')) { - toastNotifications.addDanger({ - title: xpackInfo.get('features.security.linksMessage'), - }); - kbnUrl.redirect('/management'); - return Promise.halt(); - } - }, - }, - }) - .defaults(/\/management/, { - resolve: { - securityManagementSection: function() { - const showSecurityLinks = xpackInfo.get('features.security.showLinks'); - const showRoleMappingsManagementLink = xpackInfo.get( - 'features.security.showRoleMappingsManagement' - ); - - function deregisterSecurity() { - management.deregister('security'); - } - - function deregisterRoleMappingsManagement() { - if (management.hasItem('security')) { - const security = management.getSection('security'); - if (security.hasItem('roleMappings')) { - security.deregister('roleMappings'); - } - } - } - - function ensureSecurityRegistered() { - const registerSecurity = () => - management.register('security', { - display: i18n.translate('xpack.security.management.securityTitle', { - defaultMessage: 'Security', - }), - order: 100, - icon: 'securityApp', - }); - const getSecurity = () => management.getSection('security'); - - const security = management.hasItem('security') ? getSecurity() : registerSecurity(); - - if (!security.hasItem('users')) { - security.register('users', { - name: 'securityUsersLink', - order: 10, - display: i18n.translate('xpack.security.management.usersTitle', { - defaultMessage: 'Users', - }), - url: `#${USERS_PATH}`, - }); - } - - if (!security.hasItem('roles')) { - security.register('roles', { - name: 'securityRolesLink', - order: 20, - display: i18n.translate('xpack.security.management.rolesTitle', { - defaultMessage: 'Roles', - }), - url: `#${ROLES_PATH}`, - }); - } - - if (!security.hasItem('apiKeys')) { - security.register('apiKeys', { - name: 'securityApiKeysLink', - order: 30, - display: i18n.translate('xpack.security.management.apiKeysTitle', { - defaultMessage: 'API Keys', - }), - url: `#${API_KEYS_PATH}`, - }); - } - - if (showRoleMappingsManagementLink && !security.hasItem('roleMappings')) { - security.register('roleMappings', { - name: 'securityRoleMappingLink', - order: 30, - display: i18n.translate('xpack.security.management.roleMappingsTitle', { - defaultMessage: 'Role Mappings', - }), - url: `#${ROLE_MAPPINGS_PATH}`, - }); - } - } - - if (!showSecurityLinks) { - deregisterSecurity(); - } else { - if (!showRoleMappingsManagementLink) { - deregisterRoleMappingsManagement(); - } - - // getCurrentUser will reject if there is no authenticated user, so we prevent them from - // seeing the security management screens. - return npSetup.plugins.security.authc - .getCurrentUser() - .then(ensureSecurityRegistered) - .catch(deregisterSecurity); - } - }, - }, - }); diff --git a/x-pack/legacy/plugins/security/public/views/management/password_form/password_form.html b/x-pack/legacy/plugins/security/public/views/management/password_form/password_form.html deleted file mode 100644 index 72956992100f5e..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/password_form/password_form.html +++ /dev/null @@ -1,53 +0,0 @@ - - -
- - - - -
-
- - -
- - - - -
-
-
diff --git a/x-pack/legacy/plugins/security/public/views/management/password_form/password_form.js b/x-pack/legacy/plugins/security/public/views/management/password_form/password_form.js deleted file mode 100644 index edcccdb5e6e697..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/password_form/password_form.js +++ /dev/null @@ -1,24 +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 { uiModules } from 'ui/modules'; -import template from './password_form.html'; - -const module = uiModules.get('security', ['kibana']); -module.directive('kbnPasswordForm', function() { - return { - template, - scope: { - password: '=', - }, - restrict: 'E', - replace: true, - controllerAs: 'passwordController', - controller: function() { - this.confirmation = null; - }, - }; -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/_index.scss b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/_index.scss deleted file mode 100644 index 80e08ebcf12267..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './components/rule_editor_panel/index'; \ No newline at end of file diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/edit_role_mapping_page.test.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/edit_role_mapping_page.test.tsx deleted file mode 100644 index 375a8d9f374a86..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/edit_role_mapping_page.test.tsx +++ /dev/null @@ -1,341 +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 React from 'react'; -import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; -import { findTestSubject } from 'test_utils/find_test_subject'; - -// brace/ace uses the Worker class, which is not currently provided by JSDOM. -// This is not required for the tests to pass, but it rather suppresses lengthy -// warnings in the console which adds unnecessary noise to the test output. -import 'test_utils/stub_web_worker'; - -import { RoleMappingsAPI } from '../../../../../lib/role_mappings_api'; -import { EditRoleMappingPage } from '.'; -import { NoCompatibleRealms, SectionLoading, PermissionDenied } from '../../components'; -import { VisualRuleEditor } from './rule_editor_panel/visual_rule_editor'; -import { JSONRuleEditor } from './rule_editor_panel/json_rule_editor'; -import { EuiComboBox } from '@elastic/eui'; - -jest.mock('../../../../../lib/roles_api', () => { - return { - RolesApi: { - getRoles: () => Promise.resolve([{ name: 'foo_role' }, { name: 'bar role' }]), - }, - }; -}); - -describe('EditRoleMappingPage', () => { - it('allows a role mapping to be created', async () => { - const roleMappingsAPI = ({ - saveRoleMapping: jest.fn().mockResolvedValue(null), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: true, - canUseInlineScripts: true, - canUseStoredScripts: true, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl(); - - await nextTick(); - wrapper.update(); - - findTestSubject(wrapper, 'roleMappingFormNameInput').simulate('change', { - target: { value: 'my-role-mapping' }, - }); - - (wrapper - .find(EuiComboBox) - .filter('[data-test-subj="roleMappingFormRoleComboBox"]') - .props() as any).onChange([{ label: 'foo_role' }]); - - findTestSubject(wrapper, 'roleMappingsAddRuleButton').simulate('click'); - - findTestSubject(wrapper, 'saveRoleMappingButton').simulate('click'); - - expect(roleMappingsAPI.saveRoleMapping).toHaveBeenCalledWith({ - name: 'my-role-mapping', - enabled: true, - roles: ['foo_role'], - role_templates: [], - rules: { - all: [{ field: { username: '*' } }], - }, - metadata: {}, - }); - }); - - it('allows a role mapping to be updated', async () => { - const roleMappingsAPI = ({ - saveRoleMapping: jest.fn().mockResolvedValue(null), - getRoleMapping: jest.fn().mockResolvedValue({ - name: 'foo', - role_templates: [ - { - template: { id: 'foo' }, - }, - ], - enabled: true, - rules: { - any: [{ field: { 'metadata.someCustomOption': [false, true, 'asdf'] } }], - }, - metadata: { - foo: 'bar', - bar: 'baz', - }, - }), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: true, - canUseInlineScripts: true, - canUseStoredScripts: true, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl( - - ); - - await nextTick(); - wrapper.update(); - - findTestSubject(wrapper, 'switchToRolesButton').simulate('click'); - - (wrapper - .find(EuiComboBox) - .filter('[data-test-subj="roleMappingFormRoleComboBox"]') - .props() as any).onChange([{ label: 'foo_role' }]); - - findTestSubject(wrapper, 'roleMappingsAddRuleButton').simulate('click'); - wrapper.find('button[id="addRuleOption"]').simulate('click'); - - findTestSubject(wrapper, 'saveRoleMappingButton').simulate('click'); - - expect(roleMappingsAPI.saveRoleMapping).toHaveBeenCalledWith({ - name: 'foo', - enabled: true, - roles: ['foo_role'], - role_templates: [], - rules: { - any: [ - { field: { 'metadata.someCustomOption': [false, true, 'asdf'] } }, - { field: { username: '*' } }, - ], - }, - metadata: { - foo: 'bar', - bar: 'baz', - }, - }); - }); - - it('renders a permission denied message when unauthorized to manage role mappings', async () => { - const roleMappingsAPI = ({ - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: false, - hasCompatibleRealms: true, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl(); - expect(wrapper.find(SectionLoading)).toHaveLength(1); - expect(wrapper.find(PermissionDenied)).toHaveLength(0); - - await nextTick(); - wrapper.update(); - - expect(wrapper.find(SectionLoading)).toHaveLength(0); - expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); - expect(wrapper.find(PermissionDenied)).toHaveLength(1); - }); - - it('renders a warning when there are no compatible realms enabled', async () => { - const roleMappingsAPI = ({ - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: false, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl(); - expect(wrapper.find(SectionLoading)).toHaveLength(1); - expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); - - await nextTick(); - wrapper.update(); - - expect(wrapper.find(SectionLoading)).toHaveLength(0); - expect(wrapper.find(NoCompatibleRealms)).toHaveLength(1); - }); - - it('renders a warning when editing a mapping with a stored role template, when stored scripts are disabled', async () => { - const roleMappingsAPI = ({ - getRoleMapping: jest.fn().mockResolvedValue({ - name: 'foo', - role_templates: [ - { - template: { id: 'foo' }, - }, - ], - enabled: true, - rules: { - field: { username: '*' }, - }, - }), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: true, - canUseInlineScripts: true, - canUseStoredScripts: false, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl( - - ); - - expect(findTestSubject(wrapper, 'roleMappingInlineScriptsDisabled')).toHaveLength(0); - expect(findTestSubject(wrapper, 'roleMappingStoredScriptsDisabled')).toHaveLength(0); - - await nextTick(); - wrapper.update(); - - expect(findTestSubject(wrapper, 'roleMappingInlineScriptsDisabled')).toHaveLength(0); - expect(findTestSubject(wrapper, 'roleMappingStoredScriptsDisabled')).toHaveLength(1); - }); - - it('renders a warning when editing a mapping with an inline role template, when inline scripts are disabled', async () => { - const roleMappingsAPI = ({ - getRoleMapping: jest.fn().mockResolvedValue({ - name: 'foo', - role_templates: [ - { - template: { source: 'foo' }, - }, - ], - enabled: true, - rules: { - field: { username: '*' }, - }, - }), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: true, - canUseInlineScripts: false, - canUseStoredScripts: true, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl( - - ); - - expect(findTestSubject(wrapper, 'roleMappingInlineScriptsDisabled')).toHaveLength(0); - expect(findTestSubject(wrapper, 'roleMappingStoredScriptsDisabled')).toHaveLength(0); - - await nextTick(); - wrapper.update(); - - expect(findTestSubject(wrapper, 'roleMappingInlineScriptsDisabled')).toHaveLength(1); - expect(findTestSubject(wrapper, 'roleMappingStoredScriptsDisabled')).toHaveLength(0); - }); - - it('renders the visual editor by default for simple rule sets', async () => { - const roleMappingsAPI = ({ - getRoleMapping: jest.fn().mockResolvedValue({ - name: 'foo', - roles: ['superuser'], - enabled: true, - rules: { - all: [ - { - field: { - username: '*', - }, - }, - { - field: { - dn: null, - }, - }, - { - field: { - realm: ['ldap', 'pki', null, 12], - }, - }, - ], - }, - }), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: true, - canUseInlineScripts: true, - canUseStoredScripts: true, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl( - - ); - - await nextTick(); - wrapper.update(); - - expect(wrapper.find(VisualRuleEditor)).toHaveLength(1); - expect(wrapper.find(JSONRuleEditor)).toHaveLength(0); - }); - - it('renders the JSON editor by default for complex rule sets', async () => { - const createRule = (depth: number): Record => { - if (depth > 0) { - const rule = { - all: [ - { - field: { - username: '*', - }, - }, - ], - } as Record; - - const subRule = createRule(depth - 1); - if (subRule) { - rule.all.push(subRule); - } - - return rule; - } - return null as any; - }; - - const roleMappingsAPI = ({ - getRoleMapping: jest.fn().mockResolvedValue({ - name: 'foo', - roles: ['superuser'], - enabled: true, - rules: createRule(10), - }), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: true, - canUseInlineScripts: true, - canUseStoredScripts: true, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl( - - ); - - await nextTick(); - wrapper.update(); - - expect(wrapper.find(VisualRuleEditor)).toHaveLength(0); - expect(wrapper.find(JSONRuleEditor)).toHaveLength(1); - }); -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/edit_role_mapping.html b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/edit_role_mapping.html deleted file mode 100644 index ca8ab9c35c49ba..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/edit_role_mapping.html +++ /dev/null @@ -1,3 +0,0 @@ - -
- diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/index.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/index.tsx deleted file mode 100644 index b064a4dc50a228..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/index.tsx +++ /dev/null @@ -1,45 +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 React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import routes from 'ui/routes'; -import { I18nContext } from 'ui/i18n'; -import { npSetup } from 'ui/new_platform'; -import { RoleMappingsAPI } from '../../../../lib/role_mappings_api'; -// @ts-ignore -import template from './edit_role_mapping.html'; -import { CREATE_ROLE_MAPPING_PATH } from '../../management_urls'; -import { getEditRoleMappingBreadcrumbs } from '../../breadcrumbs'; -import { EditRoleMappingPage } from './components'; - -routes.when(`${CREATE_ROLE_MAPPING_PATH}/:name?`, { - template, - k7Breadcrumbs: getEditRoleMappingBreadcrumbs, - controller($scope, $route) { - $scope.$$postDigest(() => { - const domNode = document.getElementById('editRoleMappingReactRoot'); - - const { name } = $route.current.params; - - render( - - - , - domNode - ); - - // unmount react on controller destroy - $scope.$on('$destroy', () => { - if (domNode) { - unmountComponentAtNode(domNode); - } - }); - }); - }, -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/role_mappings_grid_page.test.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/role_mappings_grid_page.test.tsx deleted file mode 100644 index 259cdc71e25a22..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/role_mappings_grid_page.test.tsx +++ /dev/null @@ -1,182 +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 React from 'react'; -import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; -import { RoleMappingsGridPage } from '.'; -import { SectionLoading, PermissionDenied, NoCompatibleRealms } from '../../components'; -import { EmptyPrompt } from './empty_prompt'; -import { findTestSubject } from 'test_utils/find_test_subject'; -import { EuiLink } from '@elastic/eui'; -import { RoleMappingsAPI } from '../../../../../lib/role_mappings_api'; -import { act } from '@testing-library/react'; - -describe('RoleMappingsGridPage', () => { - it('renders an empty prompt when no role mappings exist', async () => { - const roleMappingsAPI = ({ - getRoleMappings: jest.fn().mockResolvedValue([]), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: true, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl(); - expect(wrapper.find(SectionLoading)).toHaveLength(1); - expect(wrapper.find(EmptyPrompt)).toHaveLength(0); - - await nextTick(); - wrapper.update(); - - expect(wrapper.find(SectionLoading)).toHaveLength(0); - expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); - expect(wrapper.find(EmptyPrompt)).toHaveLength(1); - }); - - it('renders a permission denied message when unauthorized to manage role mappings', async () => { - const roleMappingsAPI = ({ - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: false, - hasCompatibleRealms: true, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl(); - expect(wrapper.find(SectionLoading)).toHaveLength(1); - expect(wrapper.find(PermissionDenied)).toHaveLength(0); - - await nextTick(); - wrapper.update(); - - expect(wrapper.find(SectionLoading)).toHaveLength(0); - expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); - expect(wrapper.find(PermissionDenied)).toHaveLength(1); - }); - - it('renders a warning when there are no compatible realms enabled', async () => { - const roleMappingsAPI = ({ - getRoleMappings: jest.fn().mockResolvedValue([ - { - name: 'some realm', - enabled: true, - roles: [], - rules: { field: { username: '*' } }, - }, - ]), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: false, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl(); - expect(wrapper.find(SectionLoading)).toHaveLength(1); - expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); - - await nextTick(); - wrapper.update(); - - expect(wrapper.find(SectionLoading)).toHaveLength(0); - expect(wrapper.find(NoCompatibleRealms)).toHaveLength(1); - }); - - it('renders links to mapped roles', async () => { - const roleMappingsAPI = ({ - getRoleMappings: jest.fn().mockResolvedValue([ - { - name: 'some realm', - enabled: true, - roles: ['superuser'], - rules: { field: { username: '*' } }, - }, - ]), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: true, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl(); - await nextTick(); - wrapper.update(); - - const links = findTestSubject(wrapper, 'roleMappingRoles').find(EuiLink); - expect(links).toHaveLength(1); - expect(links.at(0).props()).toMatchObject({ - href: '#/management/security/roles/edit/superuser', - }); - }); - - it('describes the number of mapped role templates', async () => { - const roleMappingsAPI = ({ - getRoleMappings: jest.fn().mockResolvedValue([ - { - name: 'some realm', - enabled: true, - role_templates: [{}, {}], - rules: { field: { username: '*' } }, - }, - ]), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: true, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl(); - await nextTick(); - wrapper.update(); - - const templates = findTestSubject(wrapper, 'roleMappingRoles'); - expect(templates).toHaveLength(1); - expect(templates.text()).toEqual(`2 role templates defined`); - }); - - it('allows role mappings to be deleted, refreshing the grid after', async () => { - const roleMappingsAPI = ({ - getRoleMappings: jest.fn().mockResolvedValue([ - { - name: 'some-realm', - enabled: true, - roles: ['superuser'], - rules: { field: { username: '*' } }, - }, - ]), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: true, - }), - deleteRoleMappings: jest.fn().mockReturnValue( - Promise.resolve([ - { - name: 'some-realm', - success: true, - }, - ]) - ), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl(); - await nextTick(); - wrapper.update(); - - expect(roleMappingsAPI.getRoleMappings).toHaveBeenCalledTimes(1); - expect(roleMappingsAPI.deleteRoleMappings).not.toHaveBeenCalled(); - - findTestSubject(wrapper, `deleteRoleMappingButton-some-realm`).simulate('click'); - expect(findTestSubject(wrapper, 'deleteRoleMappingConfirmationModal')).toHaveLength(1); - - await act(async () => { - findTestSubject(wrapper, 'confirmModalConfirmButton').simulate('click'); - await nextTick(); - wrapper.update(); - }); - - expect(roleMappingsAPI.deleteRoleMappings).toHaveBeenCalledWith(['some-realm']); - // Expect an additional API call to refresh the grid - expect(roleMappingsAPI.getRoleMappings).toHaveBeenCalledTimes(2); - }); -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/index.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/index.tsx deleted file mode 100644 index 9e925d0fa6dc0a..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/index.tsx +++ /dev/null @@ -1,40 +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 React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import routes from 'ui/routes'; -import { I18nContext } from 'ui/i18n'; -import { npSetup } from 'ui/new_platform'; -import { RoleMappingsAPI } from '../../../../lib/role_mappings_api'; -// @ts-ignore -import template from './role_mappings.html'; -import { ROLE_MAPPINGS_PATH } from '../../management_urls'; -import { getRoleMappingBreadcrumbs } from '../../breadcrumbs'; -import { RoleMappingsGridPage } from './components'; - -routes.when(ROLE_MAPPINGS_PATH, { - template, - k7Breadcrumbs: getRoleMappingBreadcrumbs, - controller($scope) { - $scope.$$postDigest(() => { - const domNode = document.getElementById('roleMappingsGridReactRoot'); - - render( - - - , - domNode - ); - - // unmount react on controller destroy - $scope.$on('$destroy', () => { - if (domNode) { - unmountComponentAtNode(domNode); - } - }); - }); - }, -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/role_mappings.html b/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/role_mappings.html deleted file mode 100644 index cff3b821d132c0..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/role_mappings.html +++ /dev/null @@ -1,3 +0,0 @@ - -
- diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/roles.html b/x-pack/legacy/plugins/security/public/views/management/roles_grid/roles.html deleted file mode 100644 index 0552b655afafd9..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/roles_grid/roles.html +++ /dev/null @@ -1,3 +0,0 @@ - -
- diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/roles.js b/x-pack/legacy/plugins/security/public/views/management/roles_grid/roles.js deleted file mode 100644 index e9c42824711b31..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/roles_grid/roles.js +++ /dev/null @@ -1,35 +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 React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import routes from 'ui/routes'; -import template from 'plugins/security/views/management/roles_grid/roles.html'; -import { ROLES_PATH } from '../management_urls'; -import { getRolesBreadcrumbs } from '../breadcrumbs'; -import { I18nContext } from 'ui/i18n'; -import { RolesGridPage } from './components'; - -routes.when(ROLES_PATH, { - template, - k7Breadcrumbs: getRolesBreadcrumbs, - controller($scope) { - $scope.$$postDigest(() => { - const domNode = document.getElementById('rolesGridReactRoot'); - - render( - - - , - domNode - ); - - // unmount react on controller destroy - $scope.$on('$destroy', () => { - unmountComponentAtNode(domNode); - }); - }); - }, -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/users_grid/users.html b/x-pack/legacy/plugins/security/public/views/management/users_grid/users.html deleted file mode 100644 index 3dce7326d001ae..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/users_grid/users.html +++ /dev/null @@ -1,3 +0,0 @@ - -
- diff --git a/x-pack/legacy/plugins/security/public/views/management/users_grid/users.js b/x-pack/legacy/plugins/security/public/views/management/users_grid/users.js deleted file mode 100644 index 8d4e0526251d76..00000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/users_grid/users.js +++ /dev/null @@ -1,47 +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 React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import routes from 'ui/routes'; -import template from 'plugins/security/views/management/users_grid/users.html'; -import { SECURITY_PATH, USERS_PATH } from '../management_urls'; -import { UsersListPage } from './components'; -import { UserAPIClient } from '../../../lib/api'; -import { I18nContext } from 'ui/i18n'; -import { getUsersBreadcrumbs } from '../breadcrumbs'; - -routes.when(SECURITY_PATH, { - redirectTo: USERS_PATH, -}); - -const renderReact = (elem, changeUrl) => { - render( - - - , - elem - ); -}; - -routes.when(USERS_PATH, { - template, - k7Breadcrumbs: getUsersBreadcrumbs, - controller($scope, $http, kbnUrl) { - $scope.$on('$destroy', () => { - const elem = document.getElementById('usersReactRoot'); - if (elem) unmountComponentAtNode(elem); - }); - $scope.$$postDigest(() => { - const elem = document.getElementById('usersReactRoot'); - const changeUrl = url => { - kbnUrl.change(url); - $scope.$apply(); - }; - renderReact(elem, $http, changeUrl); - }); - }, -}); diff --git a/x-pack/legacy/plugins/security/public/views/overwritten_session/overwritten_session.tsx b/x-pack/legacy/plugins/security/public/views/overwritten_session/overwritten_session.tsx index fb39c517e1c2ca..4c79c499cc0e6a 100644 --- a/x-pack/legacy/plugins/security/public/views/overwritten_session/overwritten_session.tsx +++ b/x-pack/legacy/plugins/security/public/views/overwritten_session/overwritten_session.tsx @@ -11,8 +11,7 @@ import { render } from 'react-dom'; import chrome from 'ui/chrome'; import { I18nContext } from 'ui/i18n'; import { npSetup } from 'ui/new_platform'; -import { SecurityPluginSetup } from '../../../../../../plugins/security/public'; -import { AuthenticatedUser } from '../../../common/model'; +import { AuthenticatedUser, SecurityPluginSetup } from '../../../../../../plugins/security/public'; import { AuthenticationStatePage } from '../../components/authentication_state_page'; chrome diff --git a/x-pack/plugins/security/common/model/index.ts b/x-pack/plugins/security/common/model/index.ts index f3c65ed7e3cf17..121791d113bd5d 100644 --- a/x-pack/plugins/security/common/model/index.ts +++ b/x-pack/plugins/security/common/model/index.ts @@ -10,7 +10,16 @@ export { AuthenticatedUser, canUserChangePassword } from './authenticated_user'; export { BuiltinESPrivileges } from './builtin_es_privileges'; export { FeaturesPrivileges } from './features_privileges'; export { RawKibanaPrivileges, RawKibanaFeaturePrivileges } from './raw_kibana_privileges'; -export { Role, RoleIndexPrivilege, RoleKibanaPrivilege } from './role'; +export { + Role, + RoleIndexPrivilege, + RoleKibanaPrivilege, + copyRole, + isReadOnlyRole, + isReservedRole, + isRoleEnabled, + prepareRoleClone, +} from './role'; export { KibanaPrivileges } from './kibana_privileges'; export { InlineRoleTemplate, diff --git a/x-pack/legacy/plugins/security/public/lib/role_utils.test.ts b/x-pack/plugins/security/common/model/role.test.ts similarity index 96% rename from x-pack/legacy/plugins/security/public/lib/role_utils.test.ts rename to x-pack/plugins/security/common/model/role.test.ts index 9d94017c3f0fe4..d4a910a1785ebd 100644 --- a/x-pack/legacy/plugins/security/public/lib/role_utils.test.ts +++ b/x-pack/plugins/security/common/model/role.test.ts @@ -4,14 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Role } from '../../common/model'; -import { - copyRole, - isReadOnlyRole, - isReservedRole, - isRoleEnabled, - prepareRoleClone, -} from './role_utils'; +import { Role, isReadOnlyRole, isReservedRole, isRoleEnabled, copyRole, prepareRoleClone } from '.'; describe('role', () => { describe('isRoleEnabled', () => { diff --git a/x-pack/plugins/security/common/model/role.ts b/x-pack/plugins/security/common/model/role.ts index 89f68aaa55b5cb..1edcf147262ede 100644 --- a/x-pack/plugins/security/common/model/role.ts +++ b/x-pack/plugins/security/common/model/role.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { cloneDeep } from 'lodash'; import { FeaturesPrivileges } from './features_privileges'; export interface RoleIndexPrivilege { @@ -40,3 +41,53 @@ export interface Role { _transform_error?: string[]; _unrecognized_applications?: string[]; } + +/** + * Returns whether given role is enabled or not + * + * @param role Object Role JSON, as returned by roles API + * @return Boolean true if role is enabled; false otherwise + */ +export function isRoleEnabled(role: Partial) { + return role.transient_metadata?.enabled ?? true; +} + +/** + * Returns whether given role is reserved or not. + * + * @param role Role as returned by roles API + */ +export function isReservedRole(role: Partial) { + return (role.metadata?._reserved as boolean) ?? 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?.length ?? 0) > 0; +} + +/** + * Returns a deep copy of the role. + * + * @param role the Role to copy. + */ +export function copyRole(role: Role) { + return cloneDeep(role); +} + +/** + * Creates a deep copy of the role suitable for cloning. + * + * @param role the Role to clone. + */ +export function prepareRoleClone(role: Role): Role { + const clone = copyRole(role); + + clone.name = ''; + + return clone; +} diff --git a/x-pack/plugins/security/kibana.json b/x-pack/plugins/security/kibana.json index 32f860b1423d30..7d1940e393becf 100644 --- a/x-pack/plugins/security/kibana.json +++ b/x-pack/plugins/security/kibana.json @@ -3,7 +3,8 @@ "version": "8.0.0", "kibanaVersion": "kibana", "configPath": ["xpack", "security"], - "requiredPlugins": ["features", "licensing"], + "requiredPlugins": ["data", "features", "licensing"], + "optionalPlugins": ["home", "management"], "server": true, "ui": true } diff --git a/x-pack/plugins/security/public/_index.scss b/x-pack/plugins/security/public/_index.scss new file mode 100644 index 00000000000000..9fa81bad7c3f40 --- /dev/null +++ b/x-pack/plugins/security/public/_index.scss @@ -0,0 +1,2 @@ +// Management styles +@import './management/index'; diff --git a/x-pack/legacy/plugins/security/public/views/account/components/account_management_page.test.tsx b/x-pack/plugins/security/public/account_management/account_management_page.test.tsx similarity index 72% rename from x-pack/legacy/plugins/security/public/views/account/components/account_management_page.test.tsx rename to x-pack/plugins/security/public/account_management/account_management_page.test.tsx index 366842e58e9e4a..b7cf8e6dd14181 100644 --- a/x-pack/legacy/plugins/security/public/views/account/components/account_management_page.test.tsx +++ b/x-pack/plugins/security/public/account_management/account_management_page.test.tsx @@ -6,11 +6,12 @@ import React from 'react'; import { act } from '@testing-library/react'; import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; -import { securityMock } from '../../../../../../../plugins/security/public/mocks'; +import { AuthenticatedUser } from '../../common/model'; import { AccountManagementPage } from './account_management_page'; -import { AuthenticatedUser } from '../../../../common/model'; -jest.mock('ui/kfetch'); +import { coreMock } from 'src/core/public/mocks'; +import { securityMock } from '../mocks'; +import { userAPIClientMock } from '../management/users/index.mock'; interface Options { withFullName?: boolean; @@ -45,7 +46,11 @@ describe('', () => { it(`displays users full name, username, and email address`, async () => { const user = createUser(); const wrapper = mountWithIntl( - + ); await act(async () => { @@ -63,7 +68,11 @@ describe('', () => { it(`displays username when full_name is not provided`, async () => { const user = createUser({ withFullName: false }); const wrapper = mountWithIntl( - + ); await act(async () => { @@ -77,7 +86,11 @@ describe('', () => { it(`displays a placeholder when no email address is provided`, async () => { const user = createUser({ withEmail: false }); const wrapper = mountWithIntl( - + ); await act(async () => { @@ -91,7 +104,11 @@ describe('', () => { it(`displays change password form for users in the native realm`, async () => { const user = createUser(); const wrapper = mountWithIntl( - + ); await act(async () => { @@ -106,7 +123,11 @@ describe('', () => { it(`does not display change password form for users in the saml realm`, async () => { const user = createUser({ realm: 'saml' }); const wrapper = mountWithIntl( - + ); await act(async () => { diff --git a/x-pack/legacy/plugins/security/public/views/account/components/account_management_page.tsx b/x-pack/plugins/security/public/account_management/account_management_page.tsx similarity index 62% rename from x-pack/legacy/plugins/security/public/views/account/components/account_management_page.tsx rename to x-pack/plugins/security/public/account_management/account_management_page.tsx index 6abee73e0b3535..3f764adc7949f2 100644 --- a/x-pack/legacy/plugins/security/public/views/account/components/account_management_page.tsx +++ b/x-pack/plugins/security/public/account_management/account_management_page.tsx @@ -5,20 +5,24 @@ */ import { EuiPage, EuiPageBody, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui'; import React, { useEffect, useState } from 'react'; -import { SecurityPluginSetup } from '../../../../../../../plugins/security/public'; -import { getUserDisplayName, AuthenticatedUser } from '../../../../common/model'; +import { NotificationsStart } from 'src/core/public'; +import { getUserDisplayName, AuthenticatedUser } from '../../common/model'; +import { AuthenticationServiceSetup } from '../authentication'; import { ChangePassword } from './change_password'; +import { UserAPIClient } from '../management'; import { PersonalInfo } from './personal_info'; interface Props { - securitySetup: SecurityPluginSetup; + authc: AuthenticationServiceSetup; + apiClient: PublicMethodsOf; + notifications: NotificationsStart; } -export const AccountManagementPage = (props: Props) => { +export const AccountManagementPage = ({ apiClient, authc, notifications }: Props) => { const [currentUser, setCurrentUser] = useState(null); useEffect(() => { - props.securitySetup.authc.getCurrentUser().then(setCurrentUser); - }, [props]); + authc.getCurrentUser().then(setCurrentUser); + }, [authc]); if (!currentUser) { return null; @@ -36,7 +40,7 @@ export const AccountManagementPage = (props: Props) => { - + diff --git a/x-pack/legacy/plugins/security/public/views/account/components/change_password/change_password.tsx b/x-pack/plugins/security/public/account_management/change_password/change_password.tsx similarity index 81% rename from x-pack/legacy/plugins/security/public/views/account/components/change_password/change_password.tsx rename to x-pack/plugins/security/public/account_management/change_password/change_password.tsx index 63abb4539470d2..f5ac5f3b21d2e6 100644 --- a/x-pack/legacy/plugins/security/public/views/account/components/change_password/change_password.tsx +++ b/x-pack/plugins/security/public/account_management/change_password/change_password.tsx @@ -3,18 +3,18 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { - // @ts-ignore - EuiDescribedFormGroup, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component } from 'react'; -import { UserAPIClient } from '../../../../lib/api'; -import { AuthenticatedUser, canUserChangePassword } from '../../../../../common/model'; -import { ChangePasswordForm } from '../../../../components/management/change_password_form'; +import { EuiDescribedFormGroup } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { NotificationsSetup } from 'src/core/public'; +import { AuthenticatedUser, canUserChangePassword } from '../../../common/model'; +import { UserAPIClient } from '../../management/users'; +import { ChangePasswordForm } from '../../management/users/components/change_password_form'; interface Props { user: AuthenticatedUser; + apiClient: PublicMethodsOf; + notifications: NotificationsSetup; } export class ChangePassword extends Component { @@ -48,7 +48,8 @@ export class ChangePassword extends Component { ); diff --git a/x-pack/legacy/plugins/security/public/views/account/components/change_password/index.ts b/x-pack/plugins/security/public/account_management/change_password/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/account/components/change_password/index.ts rename to x-pack/plugins/security/public/account_management/change_password/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/account/components/index.ts b/x-pack/plugins/security/public/account_management/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/account/components/index.ts rename to x-pack/plugins/security/public/account_management/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/account/components/personal_info/index.ts b/x-pack/plugins/security/public/account_management/personal_info/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/account/components/personal_info/index.ts rename to x-pack/plugins/security/public/account_management/personal_info/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/account/components/personal_info/personal_info.tsx b/x-pack/plugins/security/public/account_management/personal_info/personal_info.tsx similarity index 89% rename from x-pack/legacy/plugins/security/public/views/account/components/personal_info/personal_info.tsx rename to x-pack/plugins/security/public/account_management/personal_info/personal_info.tsx index 7121bf7ab28ee7..9cbbc242e8400e 100644 --- a/x-pack/legacy/plugins/security/public/views/account/components/personal_info/personal_info.tsx +++ b/x-pack/plugins/security/public/account_management/personal_info/personal_info.tsx @@ -3,15 +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 { - // @ts-ignore - EuiDescribedFormGroup, - EuiFormRow, - EuiText, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { AuthenticatedUser } from '../../../../../common/model'; +import { EuiDescribedFormGroup, EuiFormRow, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { AuthenticatedUser } from '../../../common/model'; interface Props { user: AuthenticatedUser; diff --git a/x-pack/plugins/security/public/management/_index.scss b/x-pack/plugins/security/public/management/_index.scss new file mode 100644 index 00000000000000..5d419b53230799 --- /dev/null +++ b/x-pack/plugins/security/public/management/_index.scss @@ -0,0 +1,3 @@ +@import './roles/index'; +@import './users/index'; +@import './role_mappings/index'; diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.mock.ts b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.mock.ts new file mode 100644 index 00000000000000..2a45d497029f41 --- /dev/null +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.mock.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 const apiKeysAPIClientMock = { + create: () => ({ + checkPrivileges: jest.fn(), + getApiKeys: jest.fn(), + invalidateApiKeys: jest.fn(), + }), +}; diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.test.ts b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.test.ts new file mode 100644 index 00000000000000..7d51a80459a6e9 --- /dev/null +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.test.ts @@ -0,0 +1,86 @@ +/* + * 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 { APIKeysAPIClient } from './api_keys_api_client'; + +import { httpServiceMock } from '../../../../../../src/core/public/mocks'; + +describe('APIKeysAPIClient', () => { + it('checkPrivileges() queries correct endpoint', async () => { + const httpMock = httpServiceMock.createStartContract(); + + const mockResponse = Symbol('mockResponse'); + httpMock.get.mockResolvedValue(mockResponse); + + const apiClient = new APIKeysAPIClient(httpMock); + + await expect(apiClient.checkPrivileges()).resolves.toBe(mockResponse); + expect(httpMock.get).toHaveBeenCalledTimes(1); + expect(httpMock.get).toHaveBeenCalledWith('/internal/security/api_key/privileges'); + }); + + it('getApiKeys() queries correct endpoint', async () => { + const httpMock = httpServiceMock.createStartContract(); + + const mockResponse = Symbol('mockResponse'); + httpMock.get.mockResolvedValue(mockResponse); + + const apiClient = new APIKeysAPIClient(httpMock); + + await expect(apiClient.getApiKeys()).resolves.toBe(mockResponse); + expect(httpMock.get).toHaveBeenCalledTimes(1); + expect(httpMock.get).toHaveBeenCalledWith('/internal/security/api_key', { + query: { isAdmin: false }, + }); + httpMock.get.mockClear(); + + await expect(apiClient.getApiKeys(false)).resolves.toBe(mockResponse); + expect(httpMock.get).toHaveBeenCalledTimes(1); + expect(httpMock.get).toHaveBeenCalledWith('/internal/security/api_key', { + query: { isAdmin: false }, + }); + httpMock.get.mockClear(); + + await expect(apiClient.getApiKeys(true)).resolves.toBe(mockResponse); + expect(httpMock.get).toHaveBeenCalledTimes(1); + expect(httpMock.get).toHaveBeenCalledWith('/internal/security/api_key', { + query: { isAdmin: true }, + }); + }); + + it('invalidateApiKeys() queries correct endpoint', async () => { + const httpMock = httpServiceMock.createStartContract(); + + const mockResponse = Symbol('mockResponse'); + httpMock.post.mockResolvedValue(mockResponse); + + const apiClient = new APIKeysAPIClient(httpMock); + const mockAPIKeys = [ + { id: 'one', name: 'name-one' }, + { id: 'two', name: 'name-two' }, + ]; + + await expect(apiClient.invalidateApiKeys(mockAPIKeys)).resolves.toBe(mockResponse); + expect(httpMock.post).toHaveBeenCalledTimes(1); + expect(httpMock.post).toHaveBeenCalledWith('/internal/security/api_key/invalidate', { + body: JSON.stringify({ apiKeys: mockAPIKeys, isAdmin: false }), + }); + httpMock.post.mockClear(); + + await expect(apiClient.invalidateApiKeys(mockAPIKeys, false)).resolves.toBe(mockResponse); + expect(httpMock.post).toHaveBeenCalledTimes(1); + expect(httpMock.post).toHaveBeenCalledWith('/internal/security/api_key/invalidate', { + body: JSON.stringify({ apiKeys: mockAPIKeys, isAdmin: false }), + }); + httpMock.post.mockClear(); + + await expect(apiClient.invalidateApiKeys(mockAPIKeys, true)).resolves.toBe(mockResponse); + expect(httpMock.post).toHaveBeenCalledTimes(1); + expect(httpMock.post).toHaveBeenCalledWith('/internal/security/api_key/invalidate', { + body: JSON.stringify({ apiKeys: mockAPIKeys, isAdmin: true }), + }); + }); +}); diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts new file mode 100644 index 00000000000000..372b1e56a73c47 --- /dev/null +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts @@ -0,0 +1,42 @@ +/* + * 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 { HttpStart } from 'src/core/public'; +import { ApiKey, ApiKeyToInvalidate } from '../../../common/model'; + +interface CheckPrivilegesResponse { + areApiKeysEnabled: boolean; + isAdmin: boolean; +} + +interface InvalidateApiKeysResponse { + itemsInvalidated: ApiKeyToInvalidate[]; + errors: any[]; +} + +interface GetApiKeysResponse { + apiKeys: ApiKey[]; +} + +const apiKeysUrl = '/internal/security/api_key'; + +export class APIKeysAPIClient { + constructor(private readonly http: HttpStart) {} + + public async checkPrivileges() { + return await this.http.get(`${apiKeysUrl}/privileges`); + } + + public async getApiKeys(isAdmin = false) { + return await this.http.get(apiKeysUrl, { query: { isAdmin } }); + } + + public async invalidateApiKeys(apiKeys: ApiKeyToInvalidate[], isAdmin = false) { + return await this.http.post(`${apiKeysUrl}/invalidate`, { + body: JSON.stringify({ apiKeys, isAdmin }), + }); + } +} diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/__snapshots__/api_keys_grid_page.test.tsx.snap b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/__snapshots__/api_keys_grid_page.test.tsx.snap similarity index 91% rename from x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/__snapshots__/api_keys_grid_page.test.tsx.snap rename to x-pack/plugins/security/public/management/api_keys/api_keys_grid/__snapshots__/api_keys_grid_page.test.tsx.snap index c2537235c99f61..42fd4417e238b1 100644 --- a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/__snapshots__/api_keys_grid_page.test.tsx.snap +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/__snapshots__/api_keys_grid_page.test.tsx.snap @@ -1,7 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ApiKeysGridPage renders a callout when API keys are not enabled 1`] = ` - +exports[`APIKeysGridPage renders a callout when API keys are not enabled 1`] = ` + Contact your system administrator and refer to the `; -exports[`ApiKeysGridPage renders permission denied if user does not have required permissions 1`] = ` +exports[`APIKeysGridPage renders permission denied if user does not have required permissions 1`] = ` ({ body: { statusCode: 403 } }); -const mock500 = () => ({ body: { error: 'Internal Server Error', message: '', statusCode: 500 } }); - -jest.mock('../../../../lib/api_keys_api', () => { - return { - ApiKeysApi: { - async checkPrivileges() { - if (mockSimulate403) { - throw mock403(); - } - - return { - isAdmin: mockIsAdmin, - areApiKeysEnabled: mockAreApiKeysEnabled, - }; - }, - async getApiKeys() { - if (mockSimulate500) { - throw mock500(); - } - - return { - apiKeys: [ - { - creation: 1571322182082, - expiration: 1571408582082, - id: '0QQZ2m0BO2XZwgJFuWTT', - invalidated: false, - name: 'my-api-key', - realm: 'reserved', - username: 'elastic', - }, - ], - }; - }, - }, - }; -}); - import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { ApiKeysGridPage } from './api_keys_grid_page'; import React from 'react'; import { ReactWrapper } from 'enzyme'; import { EuiCallOut } from '@elastic/eui'; import { NotEnabled } from './not_enabled'; import { PermissionDenied } from './permission_denied'; +import { APIKeysAPIClient } from '../api_keys_api_client'; +import { DocumentationLinksService } from '../documentation_links'; +import { APIKeysGridPage } from './api_keys_grid_page'; + +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { apiKeysAPIClientMock } from '../index.mock'; + +const mock403 = () => ({ body: { statusCode: 403 } }); +const mock500 = () => ({ body: { error: 'Internal Server Error', message: '', statusCode: 500 } }); const waitForRender = async ( wrapper: ReactWrapper, @@ -77,23 +41,51 @@ const waitForRender = async ( }); }; -describe('ApiKeysGridPage', () => { +describe('APIKeysGridPage', () => { + let apiClientMock: jest.Mocked>; beforeEach(() => { - mockSimulate403 = false; - mockSimulate500 = false; - mockAreApiKeysEnabled = true; - mockIsAdmin = true; + apiClientMock = apiKeysAPIClientMock.create(); + apiClientMock.checkPrivileges.mockResolvedValue({ + isAdmin: true, + areApiKeysEnabled: true, + }); + apiClientMock.getApiKeys.mockResolvedValue({ + apiKeys: [ + { + creation: 1571322182082, + expiration: 1571408582082, + id: '0QQZ2m0BO2XZwgJFuWTT', + invalidated: false, + name: 'my-api-key', + realm: 'reserved', + username: 'elastic', + }, + ], + }); }); + const getViewProperties = () => { + const { docLinks, notifications } = coreMock.createStart(); + return { + docLinks: new DocumentationLinksService(docLinks), + notifications, + apiKeysAPIClient: apiClientMock, + }; + }; + it('renders a loading state when fetching API keys', async () => { - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl(); expect(wrapper.find('[data-test-subj="apiKeysSectionLoading"]')).toHaveLength(1); }); it('renders a callout when API keys are not enabled', async () => { - mockAreApiKeysEnabled = false; - const wrapper = mountWithIntl(); + apiClientMock.checkPrivileges.mockResolvedValue({ + isAdmin: true, + areApiKeysEnabled: false, + }); + + const wrapper = mountWithIntl(); await waitForRender(wrapper, updatedWrapper => { return updatedWrapper.find(NotEnabled).length > 0; @@ -103,8 +95,9 @@ describe('ApiKeysGridPage', () => { }); it('renders permission denied if user does not have required permissions', async () => { - mockSimulate403 = true; - const wrapper = mountWithIntl(); + apiClientMock.checkPrivileges.mockRejectedValue(mock403()); + + const wrapper = mountWithIntl(); await waitForRender(wrapper, updatedWrapper => { return updatedWrapper.find(PermissionDenied).length > 0; @@ -114,8 +107,9 @@ describe('ApiKeysGridPage', () => { }); it('renders error callout if error fetching API keys', async () => { - mockSimulate500 = true; - const wrapper = mountWithIntl(); + apiClientMock.getApiKeys.mockRejectedValue(mock500()); + + const wrapper = mountWithIntl(); await waitForRender(wrapper, updatedWrapper => { return updatedWrapper.find(EuiCallOut).length > 0; @@ -125,7 +119,10 @@ describe('ApiKeysGridPage', () => { }); describe('Admin view', () => { - const wrapper = mountWithIntl(); + let wrapper: ReactWrapper; + beforeEach(() => { + wrapper = mountWithIntl(); + }); it('renders a callout indicating the user is an administrator', async () => { const calloutEl = 'EuiCallOut[data-test-subj="apiKeyAdminDescriptionCallOut"]'; @@ -151,8 +148,15 @@ describe('ApiKeysGridPage', () => { }); describe('Non-admin view', () => { - mockIsAdmin = false; - const wrapper = mountWithIntl(); + let wrapper: ReactWrapper; + beforeEach(() => { + apiClientMock.checkPrivileges.mockResolvedValue({ + isAdmin: false, + areApiKeysEnabled: true, + }); + + wrapper = mountWithIntl(); + }); it('does NOT render a callout indicating the user is an administrator', async () => { const descriptionEl = 'EuiText[data-test-subj="apiKeysDescriptionText"]'; diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/api_keys_grid_page.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx similarity index 90% rename from x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/api_keys_grid_page.tsx rename to x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx index 92633a4b0ef57c..779a2302cfadf9 100644 --- a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/api_keys_grid_page.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx @@ -27,16 +27,23 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import moment from 'moment-timezone'; import _ from 'lodash'; -import { toastNotifications } from 'ui/notify'; +import { NotificationsStart } from 'src/core/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SectionLoading } from '../../../../../../../../../src/plugins/es_ui_shared/public/components/section_loading'; -import { ApiKey, ApiKeyToInvalidate } from '../../../../../common/model'; -import { ApiKeysApi } from '../../../../lib/api_keys_api'; +import { SectionLoading } from '../../../../../../../src/plugins/es_ui_shared/public/components/section_loading'; +import { ApiKey, ApiKeyToInvalidate } from '../../../../common/model'; +import { APIKeysAPIClient } from '../api_keys_api_client'; +import { DocumentationLinksService } from '../documentation_links'; import { PermissionDenied } from './permission_denied'; import { EmptyPrompt } from './empty_prompt'; import { NotEnabled } from './not_enabled'; import { InvalidateProvider } from './invalidate_provider'; +interface Props { + notifications: NotificationsStart; + docLinks: DocumentationLinksService; + apiKeysAPIClient: PublicMethodsOf; +} + interface State { isLoadingApp: boolean; isLoadingTable: boolean; @@ -50,7 +57,7 @@ interface State { const DATE_FORMAT = 'MMMM Do YYYY HH:mm:ss'; -export class ApiKeysGridPage extends Component { +export class APIKeysGridPage extends Component { constructor(props: any) { super(props); this.state = { @@ -124,7 +131,7 @@ export class ApiKeysGridPage extends Component { if (!areApiKeysEnabled) { return ( - + ); } @@ -132,7 +139,7 @@ export class ApiKeysGridPage extends Component { if (!isLoadingTable && apiKeys && apiKeys.length === 0) { return ( - + ); } @@ -210,7 +217,11 @@ export class ApiKeysGridPage extends Component { const search: EuiInMemoryTableProps['search'] = { toolsLeft: selectedItems.length ? ( - + {invalidateApiKeyPrompt => { return ( { return ( - + {invalidateApiKeyPrompt => { return ( { private async checkPrivileges() { try { - const { isAdmin, areApiKeysEnabled } = await ApiKeysApi.checkPrivileges(); + const { isAdmin, areApiKeysEnabled } = await this.props.apiKeysAPIClient.checkPrivileges(); this.setState({ isAdmin, areApiKeysEnabled }); if (areApiKeysEnabled) { @@ -494,14 +509,11 @@ export class ApiKeysGridPage extends Component { if (_.get(e, 'body.statusCode') === 403) { this.setState({ permissionDenied: true, isLoadingApp: false }); } else { - toastNotifications.addDanger( - this.props.i18n.translate( - 'xpack.security.management.apiKeys.table.fetchingApiKeysErrorMessage', - { - defaultMessage: 'Error checking privileges: {message}', - values: { message: _.get(e, 'body.message', '') }, - } - ) + this.props.notifications.toasts.addDanger( + i18n.translate('xpack.security.management.apiKeys.table.fetchingApiKeysErrorMessage', { + defaultMessage: 'Error checking privileges: {message}', + values: { message: _.get(e, 'body.message', '') }, + }) ); } } @@ -520,7 +532,7 @@ export class ApiKeysGridPage extends Component { private loadApiKeys = async () => { try { const { isAdmin } = this.state; - const { apiKeys } = await ApiKeysApi.getApiKeys(isAdmin); + const { apiKeys } = await this.props.apiKeysAPIClient.getApiKeys(isAdmin); this.setState({ apiKeys }); } catch (e) { this.setState({ error: e }); diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/empty_prompt/empty_prompt.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/empty_prompt/empty_prompt.tsx similarity index 89% rename from x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/empty_prompt/empty_prompt.tsx rename to x-pack/plugins/security/public/management/api_keys/api_keys_grid/empty_prompt/empty_prompt.tsx index 957ca7010a1a0a..7d762a1ceb04e0 100644 --- a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/empty_prompt/empty_prompt.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/empty_prompt/empty_prompt.tsx @@ -7,13 +7,14 @@ import React, { Fragment } from 'react'; import { EuiEmptyPrompt, EuiButton, EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { documentationLinks } from '../../services/documentation_links'; +import { DocumentationLinksService } from '../../documentation_links'; interface Props { isAdmin: boolean; + docLinks: DocumentationLinksService; } -export const EmptyPrompt: React.FunctionComponent = ({ isAdmin }) => ( +export const EmptyPrompt: React.FunctionComponent = ({ isAdmin, docLinks }) => ( = ({ isAdmin }) => ( defaultMessage="You can create an {link} from Console." values={{ link: ( - + React.ReactElement; + notifications: NotificationsStart; + apiKeysAPIClient: PublicMethodsOf; } export type InvalidateApiKeys = ( @@ -23,7 +25,12 @@ export type InvalidateApiKeys = ( type OnSuccessCallback = (apiKeysInvalidated: ApiKeyToInvalidate[]) => void; -export const InvalidateProvider: React.FunctionComponent = ({ isAdmin, children }) => { +export const InvalidateProvider: React.FunctionComponent = ({ + isAdmin, + children, + notifications, + apiKeysAPIClient, +}) => { const [apiKeys, setApiKeys] = useState([]); const [isModalOpen, setIsModalOpen] = useState(false); const onSuccessCallback = useRef(null); @@ -48,7 +55,7 @@ export const InvalidateProvider: React.FunctionComponent = ({ isAdmin, ch let errors; try { - result = await ApiKeysApi.invalidateApiKeys(apiKeys, isAdmin); + result = await apiKeysAPIClient.invalidateApiKeys(apiKeys, isAdmin); } catch (e) { error = e; } @@ -77,7 +84,7 @@ export const InvalidateProvider: React.FunctionComponent = ({ isAdmin, ch values: { name: itemsInvalidated[0].name }, } ); - toastNotifications.addSuccess(successMessage); + notifications.toasts.addSuccess(successMessage); if (onSuccessCallback.current) { onSuccessCallback.current([...itemsInvalidated]); } @@ -106,7 +113,7 @@ export const InvalidateProvider: React.FunctionComponent = ({ isAdmin, ch values: { name: (errors && errors[0].name) || apiKeys[0].name }, } ); - toastNotifications.addDanger(errorMessage); + notifications.toasts.addDanger(errorMessage); } }; diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/not_enabled/index.ts b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/not_enabled/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/not_enabled/index.ts rename to x-pack/plugins/security/public/management/api_keys/api_keys_grid/not_enabled/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/not_enabled/not_enabled.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/not_enabled/not_enabled.tsx similarity index 78% rename from x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/not_enabled/not_enabled.tsx rename to x-pack/plugins/security/public/management/api_keys/api_keys_grid/not_enabled/not_enabled.tsx index c419e15450c1e1..08fe5425577573 100644 --- a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/not_enabled/not_enabled.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/not_enabled/not_enabled.tsx @@ -7,9 +7,13 @@ import React from 'react'; import { EuiCallOut, EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { documentationLinks } from '../../services/documentation_links'; +import { DocumentationLinksService } from '../../documentation_links'; -export const NotEnabled: React.FunctionComponent = () => ( +interface Props { + docLinks: DocumentationLinksService; +} + +export const NotEnabled: React.FunctionComponent = ({ docLinks }) => ( ( defaultMessage="Contact your system administrator and refer to the {link} to enable API keys." values={{ link: ( - + ({ + APIKeysGridPage: (props: any) => `Page: ${JSON.stringify(props)}`, +})); + +import { apiKeysManagementApp } from './api_keys_management_app'; +import { coreMock } from '../../../../../../src/core/public/mocks'; + +describe('apiKeysManagementApp', () => { + it('create() returns proper management app descriptor', () => { + const { getStartServices } = coreMock.createSetup(); + + expect(apiKeysManagementApp.create({ getStartServices: getStartServices as any })) + .toMatchInlineSnapshot(` + Object { + "id": "api_keys", + "mount": [Function], + "order": 30, + "title": "API Keys", + } + `); + }); + + it('mount() works for the `grid` page', async () => { + const { getStartServices } = coreMock.createSetup(); + const container = document.createElement('div'); + + const setBreadcrumbs = jest.fn(); + const unmount = await apiKeysManagementApp + .create({ getStartServices: getStartServices as any }) + .mount({ + basePath: '/some-base-path', + element: container, + setBreadcrumbs, + }); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: '#/some-base-path', text: 'API Keys' }]); + expect(container).toMatchInlineSnapshot(` +
+ Page: {"notifications":{"toasts":{}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"},"apiKeysAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}}} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); +}); diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx new file mode 100644 index 00000000000000..35de732b84ce9b --- /dev/null +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx @@ -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 React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { i18n } from '@kbn/i18n'; +import { CoreSetup } from 'src/core/public'; +import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; +import { PluginStartDependencies } from '../../plugin'; +import { APIKeysGridPage } from './api_keys_grid'; +import { APIKeysAPIClient } from './api_keys_api_client'; +import { DocumentationLinksService } from './documentation_links'; + +interface CreateParams { + getStartServices: CoreSetup['getStartServices']; +} + +export const apiKeysManagementApp = Object.freeze({ + id: 'api_keys', + create({ getStartServices }: CreateParams) { + return { + id: this.id, + order: 30, + title: i18n.translate('xpack.security.management.apiKeysTitle', { + defaultMessage: 'API Keys', + }), + async mount({ basePath, element, setBreadcrumbs }) { + const [{ docLinks, http, notifications, i18n: i18nStart }] = await getStartServices(); + setBreadcrumbs([ + { + text: i18n.translate('xpack.security.apiKeys.breadcrumb', { + defaultMessage: 'API Keys', + }), + href: `#${basePath}`, + }, + ]); + + render( + + + , + element + ); + + return () => { + unmountComponentAtNode(element); + }; + }, + } as RegisterManagementAppArgs; + }, +}); diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/services/documentation_links.ts b/x-pack/plugins/security/public/management/api_keys/documentation_links.ts similarity index 51% rename from x-pack/legacy/plugins/security/public/views/management/api_keys_grid/services/documentation_links.ts rename to x-pack/plugins/security/public/management/api_keys/documentation_links.ts index 1f03763eb542a3..4165c2a2372c96 100644 --- a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/services/documentation_links.ts +++ b/x-pack/plugins/security/public/management/api_keys/documentation_links.ts @@ -4,18 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; +import { DocLinksStart } from 'src/core/public'; -class DocumentationLinksService { - private esDocBasePath = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/`; +export class DocumentationLinksService { + private readonly esDocBasePath: string; - public getApiKeyServiceSettingsDocUrl(): string { + constructor(docLinks: DocLinksStart) { + this.esDocBasePath = `${docLinks.ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${docLinks.DOC_LINK_VERSION}/`; + } + + public getApiKeyServiceSettingsDocUrl() { return `${this.esDocBasePath}security-settings.html#api-key-service-settings`; } - public getCreateApiKeyDocUrl(): string { + public getCreateApiKeyDocUrl() { return `${this.esDocBasePath}security-api-create-api-key.html`; } } - -export const documentationLinks = new DocumentationLinksService(); diff --git a/x-pack/plugins/security/public/management/api_keys/index.mock.ts b/x-pack/plugins/security/public/management/api_keys/index.mock.ts new file mode 100644 index 00000000000000..3c11cd6bb9c654 --- /dev/null +++ b/x-pack/plugins/security/public/management/api_keys/index.mock.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 { apiKeysAPIClientMock } from './api_keys_api_client.mock'; diff --git a/x-pack/plugins/security/public/management/api_keys/index.ts b/x-pack/plugins/security/public/management/api_keys/index.ts new file mode 100644 index 00000000000000..e15da7d5eb4092 --- /dev/null +++ b/x-pack/plugins/security/public/management/api_keys/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 { apiKeysManagementApp } from './api_keys_management_app'; diff --git a/x-pack/plugins/security/public/management/index.ts b/x-pack/plugins/security/public/management/index.ts new file mode 100644 index 00000000000000..e1a13d66e68836 --- /dev/null +++ b/x-pack/plugins/security/public/management/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 { ManagementService } from './management_service'; +export { UserAPIClient } from './users/user_api_client'; diff --git a/x-pack/plugins/security/public/management/management_service.test.ts b/x-pack/plugins/security/public/management/management_service.test.ts new file mode 100644 index 00000000000000..53c12ad7ab12c4 --- /dev/null +++ b/x-pack/plugins/security/public/management/management_service.test.ts @@ -0,0 +1,226 @@ +/* + * 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 { BehaviorSubject } from 'rxjs'; +import { ManagementApp } from '../../../../../src/plugins/management/public'; +import { SecurityLicenseFeatures } from '../../common/licensing/license_features'; +import { ManagementService } from './management_service'; +import { usersManagementApp } from './users'; + +import { coreMock } from '../../../../../src/core/public/mocks'; +import { licenseMock } from '../../common/licensing/index.mock'; +import { securityMock } from '../mocks'; +import { rolesManagementApp } from './roles'; +import { apiKeysManagementApp } from './api_keys'; +import { roleMappingsManagementApp } from './role_mappings'; + +describe('ManagementService', () => { + describe('setup()', () => { + it('properly registers security section and its applications', () => { + const { fatalErrors, getStartServices } = coreMock.createSetup(); + const { authc } = securityMock.createSetup(); + const license = licenseMock.create(); + + const mockSection = { registerApp: jest.fn() }; + const managementSetup = { + sections: { + getSection: jest.fn(), + getAllSections: jest.fn(), + register: jest.fn().mockReturnValue(mockSection), + }, + }; + + const service = new ManagementService(); + service.setup({ + getStartServices: getStartServices as any, + license, + fatalErrors, + authc, + management: managementSetup, + }); + + expect(managementSetup.sections.register).toHaveBeenCalledTimes(1); + expect(managementSetup.sections.register).toHaveBeenCalledWith({ + id: 'security', + title: 'Security', + order: 100, + euiIconType: 'securityApp', + }); + + expect(mockSection.registerApp).toHaveBeenCalledTimes(4); + expect(mockSection.registerApp).toHaveBeenCalledWith({ + id: 'users', + mount: expect.any(Function), + order: 10, + title: 'Users', + }); + expect(mockSection.registerApp).toHaveBeenCalledWith({ + id: 'roles', + mount: expect.any(Function), + order: 20, + title: 'Roles', + }); + expect(mockSection.registerApp).toHaveBeenCalledWith({ + id: 'api_keys', + mount: expect.any(Function), + order: 30, + title: 'API Keys', + }); + expect(mockSection.registerApp).toHaveBeenCalledWith({ + id: 'role_mappings', + mount: expect.any(Function), + order: 40, + title: 'Role Mappings', + }); + }); + }); + + describe('start()', () => { + function startService(initialFeatures: Partial) { + const { fatalErrors, getStartServices } = coreMock.createSetup(); + + const licenseSubject = new BehaviorSubject( + (initialFeatures as unknown) as SecurityLicenseFeatures + ); + const license = licenseMock.create(); + license.features$ = licenseSubject; + + const service = new ManagementService(); + service.setup({ + getStartServices: getStartServices as any, + license, + fatalErrors, + authc: securityMock.createSetup().authc, + management: { + sections: { + getSection: jest.fn(), + getAllSections: jest.fn(), + register: jest.fn().mockReturnValue({ registerApp: jest.fn() }), + }, + }, + }); + + const getMockedApp = () => { + // All apps are enabled by default. + let enabled = true; + return ({ + get enabled() { + return enabled; + }, + enable: jest.fn().mockImplementation(() => { + enabled = true; + }), + disable: jest.fn().mockImplementation(() => { + enabled = false; + }), + } as unknown) as jest.Mocked; + }; + const mockApps = new Map>([ + [usersManagementApp.id, getMockedApp()], + [rolesManagementApp.id, getMockedApp()], + [apiKeysManagementApp.id, getMockedApp()], + [roleMappingsManagementApp.id, getMockedApp()], + ] as Array<[string, jest.Mocked]>); + + service.start({ + management: { + sections: { + getSection: jest + .fn() + .mockReturnValue({ getApp: jest.fn().mockImplementation(id => mockApps.get(id)) }), + getAllSections: jest.fn(), + navigateToApp: jest.fn(), + }, + legacy: undefined, + }, + }); + + return { + mockApps, + updateFeatures(features: Partial) { + licenseSubject.next((features as unknown) as SecurityLicenseFeatures); + }, + }; + } + + it('does not do anything if `showLinks` is `true` at `start`', () => { + const { mockApps } = startService({ showLinks: true, showRoleMappingsManagement: true }); + for (const [, mockApp] of mockApps) { + expect(mockApp.enable).not.toHaveBeenCalled(); + expect(mockApp.disable).not.toHaveBeenCalled(); + expect(mockApp.enabled).toBe(true); + } + }); + + it('disables all apps if `showLinks` is `false` at `start`', () => { + const { mockApps } = startService({ showLinks: false, showRoleMappingsManagement: true }); + for (const [, mockApp] of mockApps) { + expect(mockApp.enabled).toBe(false); + } + }); + + it('disables only Role Mappings app if `showLinks` is `true`, but `showRoleMappingsManagement` is `false` at `start`', () => { + const { mockApps } = startService({ showLinks: true, showRoleMappingsManagement: false }); + for (const [appId, mockApp] of mockApps) { + expect(mockApp.enabled).toBe(appId !== roleMappingsManagementApp.id); + } + }); + + it('apps are disabled if `showLinks` changes after `start`', () => { + const { mockApps, updateFeatures } = startService({ + showLinks: true, + showRoleMappingsManagement: true, + }); + for (const [, mockApp] of mockApps) { + expect(mockApp.enabled).toBe(true); + } + + updateFeatures({ showLinks: false, showRoleMappingsManagement: false }); + + for (const [, mockApp] of mockApps) { + expect(mockApp.enabled).toBe(false); + } + }); + + it('role mappings app is disabled if `showRoleMappingsManagement` changes after `start`', () => { + const { mockApps, updateFeatures } = startService({ + showLinks: true, + showRoleMappingsManagement: true, + }); + for (const [, mockApp] of mockApps) { + expect(mockApp.enabled).toBe(true); + } + + updateFeatures({ showLinks: true, showRoleMappingsManagement: false }); + + for (const [appId, mockApp] of mockApps) { + expect(mockApp.enabled).toBe(appId !== roleMappingsManagementApp.id); + } + }); + + it('apps are re-enabled if `showLinks` eventually transitions to `true` after `start`', () => { + const { mockApps, updateFeatures } = startService({ + showLinks: true, + showRoleMappingsManagement: true, + }); + for (const [, mockApp] of mockApps) { + expect(mockApp.enabled).toBe(true); + } + + updateFeatures({ showLinks: false, showRoleMappingsManagement: false }); + + for (const [, mockApp] of mockApps) { + expect(mockApp.enabled).toBe(false); + } + + updateFeatures({ showLinks: true, showRoleMappingsManagement: true }); + + for (const [, mockApp] of mockApps) { + expect(mockApp.enabled).toBe(true); + } + }); + }); +}); diff --git a/x-pack/plugins/security/public/management/management_service.ts b/x-pack/plugins/security/public/management/management_service.ts new file mode 100644 index 00000000000000..5ad3681590fbf6 --- /dev/null +++ b/x-pack/plugins/security/public/management/management_service.ts @@ -0,0 +1,95 @@ +/* + * 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 { Subscription } from 'rxjs'; +import { i18n } from '@kbn/i18n'; +import { CoreSetup, FatalErrorsSetup } from 'src/core/public'; +import { + ManagementApp, + ManagementSetup, + ManagementStart, +} from '../../../../../src/plugins/management/public'; +import { SecurityLicense } from '../../common/licensing'; +import { AuthenticationServiceSetup } from '../authentication'; +import { PluginStartDependencies } from '../plugin'; +import { apiKeysManagementApp } from './api_keys'; +import { roleMappingsManagementApp } from './role_mappings'; +import { rolesManagementApp } from './roles'; +import { usersManagementApp } from './users'; + +interface SetupParams { + management: ManagementSetup; + license: SecurityLicense; + authc: AuthenticationServiceSetup; + fatalErrors: FatalErrorsSetup; + getStartServices: CoreSetup['getStartServices']; +} + +interface StartParams { + management: ManagementStart; +} + +export class ManagementService { + private license!: SecurityLicense; + private licenseFeaturesSubscription?: Subscription; + + setup({ getStartServices, management, authc, license, fatalErrors }: SetupParams) { + this.license = license; + + const securitySection = management.sections.register({ + id: 'security', + title: i18n.translate('xpack.security.management.securityTitle', { + defaultMessage: 'Security', + }), + order: 100, + euiIconType: 'securityApp', + }); + + securitySection.registerApp(usersManagementApp.create({ authc, getStartServices })); + securitySection.registerApp( + rolesManagementApp.create({ fatalErrors, license, getStartServices }) + ); + securitySection.registerApp(apiKeysManagementApp.create({ getStartServices })); + securitySection.registerApp(roleMappingsManagementApp.create({ getStartServices })); + } + + start({ management }: StartParams) { + this.licenseFeaturesSubscription = this.license.features$.subscribe(async features => { + const securitySection = management.sections.getSection('security')!; + + const securityManagementAppsStatuses: Array<[ManagementApp, boolean]> = [ + [securitySection.getApp(usersManagementApp.id)!, features.showLinks], + [securitySection.getApp(rolesManagementApp.id)!, features.showLinks], + [securitySection.getApp(apiKeysManagementApp.id)!, features.showLinks], + [ + securitySection.getApp(roleMappingsManagementApp.id)!, + features.showLinks && features.showRoleMappingsManagement, + ], + ]; + + // Iterate over all registered apps and update their enable status depending on the available + // license features. + for (const [app, enableStatus] of securityManagementAppsStatuses) { + if (app.enabled === enableStatus) { + continue; + } + + if (enableStatus) { + app.enable(); + } else { + app.disable(); + } + } + }); + } + + stop() { + if (this.licenseFeaturesSubscription) { + this.licenseFeaturesSubscription.unsubscribe(); + this.licenseFeaturesSubscription = undefined; + } + } +} diff --git a/x-pack/legacy/plugins/security/public/views/management/management_urls.ts b/x-pack/plugins/security/public/management/management_urls.ts similarity index 74% rename from x-pack/legacy/plugins/security/public/views/management/management_urls.ts rename to x-pack/plugins/security/public/management/management_urls.ts index 881740c0b2895b..0d4e3fc920bdba 100644 --- a/x-pack/legacy/plugins/security/public/views/management/management_urls.ts +++ b/x-pack/plugins/security/public/management/management_urls.ts @@ -4,19 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -export const MANAGEMENT_PATH = '/management'; -export const SECURITY_PATH = `${MANAGEMENT_PATH}/security`; +const MANAGEMENT_PATH = '/management'; +const SECURITY_PATH = `${MANAGEMENT_PATH}/security`; export const ROLES_PATH = `${SECURITY_PATH}/roles`; export const EDIT_ROLES_PATH = `${ROLES_PATH}/edit`; export const CLONE_ROLES_PATH = `${ROLES_PATH}/clone`; export const USERS_PATH = `${SECURITY_PATH}/users`; export const EDIT_USERS_PATH = `${USERS_PATH}/edit`; -export const API_KEYS_PATH = `${SECURITY_PATH}/api_keys`; export const ROLE_MAPPINGS_PATH = `${SECURITY_PATH}/role_mappings`; -export const CREATE_ROLE_MAPPING_PATH = `${ROLE_MAPPINGS_PATH}/edit`; +const CREATE_ROLE_MAPPING_PATH = `${ROLE_MAPPINGS_PATH}/edit`; export const getEditRoleHref = (roleName: string) => - `#${EDIT_ROLES_PATH}/${encodeURIComponent(roleName)}`; + `#${ROLES_PATH}/edit/${encodeURIComponent(roleName)}`; export const getCreateRoleMappingHref = () => `#${CREATE_ROLE_MAPPING_PATH}`; diff --git a/x-pack/plugins/security/public/management/role_mappings/_index.scss b/x-pack/plugins/security/public/management/role_mappings/_index.scss new file mode 100644 index 00000000000000..bae6effcd2ec56 --- /dev/null +++ b/x-pack/plugins/security/public/management/role_mappings/_index.scss @@ -0,0 +1 @@ +@import './edit_role_mapping/index'; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/delete_provider/delete_provider.test.tsx b/x-pack/plugins/security/public/management/role_mappings/components/delete_provider/delete_provider.test.tsx similarity index 64% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/components/delete_provider/delete_provider.test.tsx rename to x-pack/plugins/security/public/management/role_mappings/components/delete_provider/delete_provider.test.tsx index b826d68053e276..69142b1ad610e6 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/delete_provider/delete_provider.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/components/delete_provider/delete_provider.test.tsx @@ -5,24 +5,15 @@ */ import React from 'react'; -import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; -import { DeleteProvider } from '.'; -import { RoleMappingsAPI } from '../../../../../lib/role_mappings_api'; -import { RoleMapping } from '../../../../../../common/model'; import { EuiConfirmModal } from '@elastic/eui'; -import { findTestSubject } from 'test_utils/find_test_subject'; import { act } from '@testing-library/react'; -import { toastNotifications } from 'ui/notify'; - -jest.mock('ui/notify', () => { - return { - toastNotifications: { - addError: jest.fn(), - addSuccess: jest.fn(), - addDanger: jest.fn(), - }, - }; -}); +import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; +import { findTestSubject } from 'test_utils/find_test_subject'; +import { RoleMapping } from '../../../../../common/model'; +import { DeleteProvider } from '.'; + +import { roleMappingsAPIClientMock } from '../../index.mock'; +import { coreMock } from '../../../../../../../../src/core/public/mocks'; describe('DeleteProvider', () => { beforeEach(() => { @@ -30,17 +21,14 @@ describe('DeleteProvider', () => { }); it('allows a single role mapping to be deleted', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.deleteRoleMappings.mockResolvedValue([{ name: 'delete-me', success: true }]); + + const notifications = coreMock.createStart().notifications; + const props = { - roleMappingsAPI: ({ - deleteRoleMappings: jest.fn().mockReturnValue( - Promise.resolve([ - { - name: 'delete-me', - success: true, - }, - ]) - ), - } as unknown) as RoleMappingsAPI, + roleMappingsAPI, + notifications, }; const roleMappingsToDelete = [ @@ -79,11 +67,10 @@ describe('DeleteProvider', () => { expect(props.roleMappingsAPI.deleteRoleMappings).toHaveBeenCalledWith(['delete-me']); - const notifications = toastNotifications as jest.Mocked; - expect(notifications.addError).toHaveBeenCalledTimes(0); - expect(notifications.addDanger).toHaveBeenCalledTimes(0); - expect(notifications.addSuccess).toHaveBeenCalledTimes(1); - expect(notifications.addSuccess.mock.calls[0]).toMatchInlineSnapshot(` + expect(notifications.toasts.addError).toHaveBeenCalledTimes(0); + expect(notifications.toasts.addDanger).toHaveBeenCalledTimes(0); + expect(notifications.toasts.addSuccess).toHaveBeenCalledTimes(1); + expect(notifications.toasts.addSuccess.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { "data-test-subj": "deletedRoleMappingSuccessToast", @@ -94,21 +81,23 @@ describe('DeleteProvider', () => { }); it('allows multiple role mappings to be deleted', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.deleteRoleMappings.mockResolvedValue([ + { + name: 'delete-me', + success: true, + }, + { + name: 'delete-me-too', + success: true, + }, + ]); + + const notifications = coreMock.createStart().notifications; + const props = { - roleMappingsAPI: ({ - deleteRoleMappings: jest.fn().mockReturnValue( - Promise.resolve([ - { - name: 'delete-me', - success: true, - }, - { - name: 'delete-me-too', - success: true, - }, - ]) - ), - } as unknown) as RoleMappingsAPI, + roleMappingsAPI, + notifications, }; const roleMappingsToDelete = [ @@ -152,11 +141,11 @@ describe('DeleteProvider', () => { 'delete-me', 'delete-me-too', ]); - const notifications = toastNotifications as jest.Mocked; - expect(notifications.addError).toHaveBeenCalledTimes(0); - expect(notifications.addDanger).toHaveBeenCalledTimes(0); - expect(notifications.addSuccess).toHaveBeenCalledTimes(1); - expect(notifications.addSuccess.mock.calls[0]).toMatchInlineSnapshot(` + + expect(notifications.toasts.addError).toHaveBeenCalledTimes(0); + expect(notifications.toasts.addDanger).toHaveBeenCalledTimes(0); + expect(notifications.toasts.addSuccess).toHaveBeenCalledTimes(1); + expect(notifications.toasts.addSuccess.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { "data-test-subj": "deletedRoleMappingSuccessToast", @@ -167,22 +156,24 @@ describe('DeleteProvider', () => { }); it('handles mixed success/failure conditions', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.deleteRoleMappings.mockResolvedValue([ + { + name: 'delete-me', + success: true, + }, + { + name: 'i-wont-work', + success: false, + error: new Error('something went wrong. sad.'), + }, + ]); + + const notifications = coreMock.createStart().notifications; + const props = { - roleMappingsAPI: ({ - deleteRoleMappings: jest.fn().mockReturnValue( - Promise.resolve([ - { - name: 'delete-me', - success: true, - }, - { - name: 'i-wont-work', - success: false, - error: new Error('something went wrong. sad.'), - }, - ]) - ), - } as unknown) as RoleMappingsAPI, + roleMappingsAPI, + notifications, }; const roleMappingsToDelete = [ @@ -223,10 +214,9 @@ describe('DeleteProvider', () => { 'i-wont-work', ]); - const notifications = toastNotifications as jest.Mocked; - expect(notifications.addError).toHaveBeenCalledTimes(0); - expect(notifications.addSuccess).toHaveBeenCalledTimes(1); - expect(notifications.addSuccess.mock.calls[0]).toMatchInlineSnapshot(` + expect(notifications.toasts.addError).toHaveBeenCalledTimes(0); + expect(notifications.toasts.addSuccess).toHaveBeenCalledTimes(1); + expect(notifications.toasts.addSuccess.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { "data-test-subj": "deletedRoleMappingSuccessToast", @@ -235,8 +225,8 @@ describe('DeleteProvider', () => { ] `); - expect(notifications.addDanger).toHaveBeenCalledTimes(1); - expect(notifications.addDanger.mock.calls[0]).toMatchInlineSnapshot(` + expect(notifications.toasts.addDanger).toHaveBeenCalledTimes(1); + expect(notifications.toasts.addDanger.mock.calls[0]).toMatchInlineSnapshot(` Array [ "Error deleting role mapping 'i-wont-work'", ] @@ -244,12 +234,13 @@ describe('DeleteProvider', () => { }); it('handles errors calling the API', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.deleteRoleMappings.mockRejectedValue(new Error('AHHHHH')); + + const notifications = coreMock.createStart().notifications; const props = { - roleMappingsAPI: ({ - deleteRoleMappings: jest.fn().mockImplementation(() => { - throw new Error('AHHHHH'); - }), - } as unknown) as RoleMappingsAPI, + roleMappingsAPI, + notifications, }; const roleMappingsToDelete = [ @@ -284,12 +275,11 @@ describe('DeleteProvider', () => { expect(props.roleMappingsAPI.deleteRoleMappings).toHaveBeenCalledWith(['delete-me']); - const notifications = toastNotifications as jest.Mocked; - expect(notifications.addDanger).toHaveBeenCalledTimes(0); - expect(notifications.addSuccess).toHaveBeenCalledTimes(0); + expect(notifications.toasts.addDanger).toHaveBeenCalledTimes(0); + expect(notifications.toasts.addSuccess).toHaveBeenCalledTimes(0); - expect(notifications.addError).toHaveBeenCalledTimes(1); - expect(notifications.addError.mock.calls[0]).toMatchInlineSnapshot(` + expect(notifications.toasts.addError).toHaveBeenCalledTimes(1); + expect(notifications.toasts.addError.mock.calls[0]).toMatchInlineSnapshot(` Array [ [Error: AHHHHH], Object { diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/delete_provider/delete_provider.tsx b/x-pack/plugins/security/public/management/role_mappings/components/delete_provider/delete_provider.tsx similarity index 93% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/components/delete_provider/delete_provider.tsx rename to x-pack/plugins/security/public/management/role_mappings/components/delete_provider/delete_provider.tsx index 2072cedeab4628..860fe22cb8032c 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/delete_provider/delete_provider.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/components/delete_provider/delete_provider.tsx @@ -6,13 +6,14 @@ import React, { Fragment, useRef, useState, ReactElement } from 'react'; import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; -import { toastNotifications } from 'ui/notify'; import { i18n } from '@kbn/i18n'; -import { RoleMapping } from '../../../../../../common/model'; -import { RoleMappingsAPI } from '../../../../../lib/role_mappings_api'; +import { NotificationsStart } from 'src/core/public'; +import { RoleMapping } from '../../../../../common/model'; +import { RoleMappingsAPIClient } from '../../role_mappings_api_client'; interface Props { - roleMappingsAPI: RoleMappingsAPI; + roleMappingsAPI: PublicMethodsOf; + notifications: NotificationsStart; children: (deleteMappings: DeleteRoleMappings) => ReactElement; } @@ -23,7 +24,11 @@ export type DeleteRoleMappings = ( type OnSuccessCallback = (deletedRoleMappings: string[]) => void; -export const DeleteProvider: React.FunctionComponent = ({ roleMappingsAPI, children }) => { +export const DeleteProvider: React.FunctionComponent = ({ + roleMappingsAPI, + children, + notifications, +}) => { const [roleMappings, setRoleMappings] = useState([]); const [isModalOpen, setIsModalOpen] = useState(false); const [isDeleteInProgress, setIsDeleteInProgress] = useState(false); @@ -55,7 +60,7 @@ export const DeleteProvider: React.FunctionComponent = ({ roleMappingsAPI try { result = await roleMappingsAPI.deleteRoleMappings(roleMappings.map(rm => rm.name)); } catch (e) { - toastNotifications.addError(e, { + notifications.toasts.addError(e, { title: i18n.translate( 'xpack.security.management.roleMappings.deleteRoleMapping.unknownError', { @@ -92,7 +97,7 @@ export const DeleteProvider: React.FunctionComponent = ({ roleMappingsAPI values: { name: successfulDeletes[0].name }, } ); - toastNotifications.addSuccess({ + notifications.toasts.addSuccess({ title: successMessage, 'data-test-subj': 'deletedRoleMappingSuccessToast', }); @@ -121,7 +126,7 @@ export const DeleteProvider: React.FunctionComponent = ({ roleMappingsAPI values: { name: erroredDeletes[0].name }, } ); - toastNotifications.addDanger(errorMessage); + notifications.toasts.addDanger(errorMessage); } }; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/delete_provider/index.ts b/x-pack/plugins/security/public/management/role_mappings/components/delete_provider/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/components/delete_provider/index.ts rename to x-pack/plugins/security/public/management/role_mappings/components/delete_provider/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/index.ts b/x-pack/plugins/security/public/management/role_mappings/components/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/components/index.ts rename to x-pack/plugins/security/public/management/role_mappings/components/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/no_compatible_realms/index.ts b/x-pack/plugins/security/public/management/role_mappings/components/no_compatible_realms/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/components/no_compatible_realms/index.ts rename to x-pack/plugins/security/public/management/role_mappings/components/no_compatible_realms/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/no_compatible_realms/no_compatible_realms.tsx b/x-pack/plugins/security/public/management/role_mappings/components/no_compatible_realms/no_compatible_realms.tsx similarity index 78% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/components/no_compatible_realms/no_compatible_realms.tsx rename to x-pack/plugins/security/public/management/role_mappings/components/no_compatible_realms/no_compatible_realms.tsx index 969832b3ecbae1..5e14b0c179bfd7 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/no_compatible_realms/no_compatible_realms.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/components/no_compatible_realms/no_compatible_realms.tsx @@ -7,9 +7,13 @@ import React from 'react'; import { EuiCallOut, EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { documentationLinks } from '../../services/documentation_links'; +import { DocumentationLinksService } from '../../documentation_links'; -export const NoCompatibleRealms: React.FunctionComponent = () => ( +interface Props { + docLinks: DocumentationLinksService; +} + +export const NoCompatibleRealms: React.FunctionComponent = ({ docLinks }: Props) => ( ( defaultMessage="Role mappings will not be applied to any users. Contact your system administrator and refer to the {link} for more information." values={{ link: ( - + { + let rolesAPI: PublicMethodsOf; + beforeEach(() => { + rolesAPI = rolesAPIClientMock.create(); + (rolesAPI as jest.Mocked).getRoles.mockResolvedValue([ + { name: 'foo_role' }, + { name: 'bar role' }, + ] as Role[]); + }); + + it('allows a role mapping to be created', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.saveRoleMapping.mockResolvedValue(null); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + canUseInlineScripts: true, + canUseStoredScripts: true, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + + await nextTick(); + wrapper.update(); + + findTestSubject(wrapper, 'roleMappingFormNameInput').simulate('change', { + target: { value: 'my-role-mapping' }, + }); + + (wrapper + .find(EuiComboBox) + .filter('[data-test-subj="roleMappingFormRoleComboBox"]') + .props() as any).onChange([{ label: 'foo_role' }]); + + findTestSubject(wrapper, 'roleMappingsAddRuleButton').simulate('click'); + + findTestSubject(wrapper, 'saveRoleMappingButton').simulate('click'); + + expect(roleMappingsAPI.saveRoleMapping).toHaveBeenCalledWith({ + name: 'my-role-mapping', + enabled: true, + roles: ['foo_role'], + role_templates: [], + rules: { + all: [{ field: { username: '*' } }], + }, + metadata: {}, + }); + }); + + it('allows a role mapping to be updated', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.saveRoleMapping.mockResolvedValue(null); + roleMappingsAPI.getRoleMapping.mockResolvedValue({ + name: 'foo', + role_templates: [ + { + template: { id: 'foo' }, + }, + ], + enabled: true, + rules: { + any: [{ field: { 'metadata.someCustomOption': [false, true, 'asdf'] } }], + }, + metadata: { + foo: 'bar', + bar: 'baz', + }, + }); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + canUseInlineScripts: true, + canUseStoredScripts: true, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + + await nextTick(); + wrapper.update(); + + findTestSubject(wrapper, 'switchToRolesButton').simulate('click'); + + (wrapper + .find(EuiComboBox) + .filter('[data-test-subj="roleMappingFormRoleComboBox"]') + .props() as any).onChange([{ label: 'foo_role' }]); + + findTestSubject(wrapper, 'roleMappingsAddRuleButton').simulate('click'); + wrapper.find('button[id="addRuleOption"]').simulate('click'); + + findTestSubject(wrapper, 'saveRoleMappingButton').simulate('click'); + + expect(roleMappingsAPI.saveRoleMapping).toHaveBeenCalledWith({ + name: 'foo', + enabled: true, + roles: ['foo_role'], + role_templates: [], + rules: { + any: [ + { field: { 'metadata.someCustomOption': [false, true, 'asdf'] } }, + { field: { username: '*' } }, + ], + }, + metadata: { + foo: 'bar', + bar: 'baz', + }, + }); + }); + + it('renders a permission denied message when unauthorized to manage role mappings', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: false, + hasCompatibleRealms: true, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + expect(wrapper.find(SectionLoading)).toHaveLength(1); + expect(wrapper.find(PermissionDenied)).toHaveLength(0); + + await nextTick(); + wrapper.update(); + + expect(wrapper.find(SectionLoading)).toHaveLength(0); + expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); + expect(wrapper.find(PermissionDenied)).toHaveLength(1); + }); + + it('renders a warning when there are no compatible realms enabled', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: false, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + expect(wrapper.find(SectionLoading)).toHaveLength(1); + expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); + + await nextTick(); + wrapper.update(); + + expect(wrapper.find(SectionLoading)).toHaveLength(0); + expect(wrapper.find(NoCompatibleRealms)).toHaveLength(1); + }); + + it('renders a warning when editing a mapping with a stored role template, when stored scripts are disabled', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.getRoleMapping.mockResolvedValue({ + name: 'foo', + role_templates: [ + { + template: { id: 'foo' }, + }, + ], + enabled: true, + rules: { + field: { username: '*' }, + }, + }); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + canUseInlineScripts: true, + canUseStoredScripts: false, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + + expect(findTestSubject(wrapper, 'roleMappingInlineScriptsDisabled')).toHaveLength(0); + expect(findTestSubject(wrapper, 'roleMappingStoredScriptsDisabled')).toHaveLength(0); + + await nextTick(); + wrapper.update(); + + expect(findTestSubject(wrapper, 'roleMappingInlineScriptsDisabled')).toHaveLength(0); + expect(findTestSubject(wrapper, 'roleMappingStoredScriptsDisabled')).toHaveLength(1); + }); + + it('renders a warning when editing a mapping with an inline role template, when inline scripts are disabled', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.getRoleMapping.mockResolvedValue({ + name: 'foo', + role_templates: [ + { + template: { source: 'foo' }, + }, + ], + enabled: true, + rules: { + field: { username: '*' }, + }, + }); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + canUseInlineScripts: false, + canUseStoredScripts: true, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + + expect(findTestSubject(wrapper, 'roleMappingInlineScriptsDisabled')).toHaveLength(0); + expect(findTestSubject(wrapper, 'roleMappingStoredScriptsDisabled')).toHaveLength(0); + + await nextTick(); + wrapper.update(); + + expect(findTestSubject(wrapper, 'roleMappingInlineScriptsDisabled')).toHaveLength(1); + expect(findTestSubject(wrapper, 'roleMappingStoredScriptsDisabled')).toHaveLength(0); + }); + + it('renders the visual editor by default for simple rule sets', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.getRoleMapping.mockResolvedValue({ + name: 'foo', + roles: ['superuser'], + enabled: true, + rules: { + all: [ + { + field: { + username: '*', + }, + }, + { + field: { + dn: null, + }, + }, + { + field: { + realm: ['ldap', 'pki', null, 12], + }, + }, + ], + }, + }); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + canUseInlineScripts: true, + canUseStoredScripts: true, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + + await nextTick(); + wrapper.update(); + + expect(wrapper.find(VisualRuleEditor)).toHaveLength(1); + expect(wrapper.find(JSONRuleEditor)).toHaveLength(0); + }); + + it('renders the JSON editor by default for complex rule sets', async () => { + const createRule = (depth: number): Record => { + if (depth > 0) { + const rule = { + all: [ + { + field: { + username: '*', + }, + }, + ], + } as Record; + + const subRule = createRule(depth - 1); + if (subRule) { + rule.all.push(subRule); + } + + return rule; + } + return null as any; + }; + + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.getRoleMapping.mockResolvedValue({ + name: 'foo', + roles: ['superuser'], + enabled: true, + rules: createRule(10), + }); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + canUseInlineScripts: true, + canUseStoredScripts: true, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + + await nextTick(); + wrapper.update(); + + expect(wrapper.find(VisualRuleEditor)).toHaveLength(0); + expect(wrapper.find(JSONRuleEditor)).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/edit_role_mapping_page.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.tsx similarity index 87% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/edit_role_mapping_page.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.tsx index b8a75a4ad9fdf7..142b53cbb50f29 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/edit_role_mapping_page.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.tsx @@ -19,20 +19,21 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { toastNotifications } from 'ui/notify'; -import { RoleMapping } from '../../../../../../common/model'; -import { RoleMappingsAPI } from '../../../../../lib/role_mappings_api'; +import { NotificationsStart } from 'src/core/public'; +import { RoleMapping } from '../../../../common/model'; import { RuleEditorPanel } from './rule_editor_panel'; import { NoCompatibleRealms, PermissionDenied, DeleteProvider, SectionLoading, -} from '../../components'; -import { ROLE_MAPPINGS_PATH } from '../../../management_urls'; -import { validateRoleMappingForSave } from '../services/role_mapping_validation'; +} from '../components'; +import { RolesAPIClient } from '../../roles'; +import { ROLE_MAPPINGS_PATH } from '../../management_urls'; +import { validateRoleMappingForSave } from './services/role_mapping_validation'; import { MappingInfoPanel } from './mapping_info_panel'; -import { documentationLinks } from '../../services/documentation_links'; +import { DocumentationLinksService } from '../documentation_links'; +import { RoleMappingsAPIClient } from '../role_mappings_api_client'; interface State { loadState: 'loading' | 'permissionDenied' | 'ready' | 'saveInProgress'; @@ -50,7 +51,10 @@ interface State { interface Props { name?: string; - roleMappingsAPI: RoleMappingsAPI; + roleMappingsAPI: PublicMethodsOf; + rolesAPIClient: PublicMethodsOf; + notifications: NotificationsStart; + docLinks: DocumentationLinksService; } export class EditRoleMappingPage extends Component { @@ -74,6 +78,12 @@ export class EditRoleMappingPage extends Component { this.loadAppData(); } + public async componentDidUpdate(prevProps: Props) { + if (prevProps.name !== this.props.name) { + await this.loadAppData(); + } + } + public render() { const { loadState } = this.state; @@ -101,6 +111,8 @@ export class EditRoleMappingPage extends Component { validateForm={this.state.validateForm} canUseInlineScripts={this.state.canUseInlineScripts} canUseStoredScripts={this.state.canUseStoredScripts} + rolesAPIClient={this.props.rolesAPIClient} + docLinks={this.props.docLinks} /> { }, }) } + docLinks={this.props.docLinks} /> {this.getFormButtons()} @@ -149,7 +162,7 @@ export class EditRoleMappingPage extends Component { values={{ learnMoreLink: ( @@ -166,7 +179,7 @@ export class EditRoleMappingPage extends Component { {!this.state.hasCompatibleRealms && ( <> - + )} @@ -201,7 +214,10 @@ export class EditRoleMappingPage extends Component { {this.editingExistingRoleMapping() && ( - + {deleteRoleMappingsPrompt => { return ( { this.props.roleMappingsAPI .saveRoleMapping(this.state.roleMapping) .then(() => { - toastNotifications.addSuccess({ + this.props.notifications.toasts.addSuccess({ title: i18n.translate('xpack.security.management.editRoleMapping.saveSuccess', { defaultMessage: `Saved role mapping '{roleMappingName}'`, values: { @@ -264,7 +280,7 @@ export class EditRoleMappingPage extends Component { this.backToRoleMappingsList(); }) .catch(e => { - toastNotifications.addError(e, { + this.props.notifications.toasts.addError(e, { title: i18n.translate('xpack.security.management.editRoleMapping.saveError', { defaultMessage: `Error saving role mapping`, }), @@ -312,7 +328,7 @@ export class EditRoleMappingPage extends Component { roleMapping, }); } catch (e) { - toastNotifications.addDanger({ + this.props.notifications.toasts.addDanger({ title: i18n.translate( 'xpack.security.management.editRoleMapping.table.fetchingRoleMappingsErrorMessage', { diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/index.ts b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/index.ts rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/mapping_info_panel/index.ts b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/mapping_info_panel/index.ts rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/mapping_info_panel/mapping_info_panel.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.test.tsx similarity index 82% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/mapping_info_panel/mapping_info_panel.test.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.test.tsx index d821b33ace6a7b..9b62ca27ca569c 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/mapping_info_panel/mapping_info_panel.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.test.tsx @@ -6,21 +6,27 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { MappingInfoPanel } from '.'; -import { RoleMapping } from '../../../../../../../common/model'; import { findTestSubject } from 'test_utils/find_test_subject'; +import { Role, RoleMapping } from '../../../../../common/model'; +import { RolesAPIClient } from '../../../roles'; +import { DocumentationLinksService } from '../../documentation_links'; import { RoleSelector } from '../role_selector'; import { RoleTemplateEditor } from '../role_selector/role_template_editor'; +import { MappingInfoPanel } from '.'; -jest.mock('../../../../../../lib/roles_api', () => { - return { - RolesApi: { - getRoles: () => Promise.resolve([{ name: 'foo_role' }, { name: 'bar role' }]), - }, - }; -}); +import { rolesAPIClientMock } from '../../../roles/roles_api_client.mock'; +import { coreMock } from '../../../../../../../../src/core/public/mocks'; describe('MappingInfoPanel', () => { + let rolesAPI: PublicMethodsOf; + beforeEach(() => { + rolesAPI = rolesAPIClientMock.create(); + (rolesAPI as jest.Mocked).getRoles.mockResolvedValue([ + { name: 'foo_role' }, + { name: 'bar role' }, + ] as Role[]); + }); + it('renders when creating a role mapping, default to the "roles" view', () => { const props = { roleMapping: { @@ -32,6 +38,8 @@ describe('MappingInfoPanel', () => { metadata: {}, } as RoleMapping, mode: 'create', + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), + rolesAPIClient: rolesAPI, } as MappingInfoPanel['props']; const wrapper = mountWithIntl(); @@ -77,6 +85,8 @@ describe('MappingInfoPanel', () => { metadata: {}, } as RoleMapping, mode: 'edit', + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), + rolesAPIClient: rolesAPI, } as MappingInfoPanel['props']; const wrapper = mountWithIntl(); @@ -101,6 +111,8 @@ describe('MappingInfoPanel', () => { canUseInlineScripts: true, canUseStoredScripts: false, validateForm: false, + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), + rolesAPIClient: rolesAPI, }; const wrapper = mountWithIntl(); @@ -140,6 +152,8 @@ describe('MappingInfoPanel', () => { canUseInlineScripts: false, canUseStoredScripts: true, validateForm: false, + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), + rolesAPIClient: rolesAPI, }; const wrapper = mountWithIntl(); @@ -179,6 +193,8 @@ describe('MappingInfoPanel', () => { canUseInlineScripts: false, canUseStoredScripts: false, validateForm: false, + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), + rolesAPIClient: rolesAPI, }; const wrapper = mountWithIntl(); @@ -202,6 +218,8 @@ describe('MappingInfoPanel', () => { metadata: {}, } as RoleMapping, mode: 'edit', + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), + rolesAPIClient: rolesAPI, } as MappingInfoPanel['props']; const wrapper = mountWithIntl(); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/mapping_info_panel/mapping_info_panel.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.tsx similarity index 95% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/mapping_info_panel/mapping_info_panel.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.tsx index a02b4fc1709f02..02af6bfbafa7e0 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/mapping_info_panel/mapping_info_panel.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.tsx @@ -18,14 +18,15 @@ import { EuiSwitch, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { RoleMapping } from '../../../../../../../common/model'; +import { RoleMapping } from '../../../../../common/model'; +import { RolesAPIClient } from '../../../roles'; import { validateRoleMappingName, validateRoleMappingRoles, validateRoleMappingRoleTemplates, -} from '../../services/role_mapping_validation'; +} from '../services/role_mapping_validation'; import { RoleSelector } from '../role_selector'; -import { documentationLinks } from '../../../services/documentation_links'; +import { DocumentationLinksService } from '../../documentation_links'; interface Props { roleMapping: RoleMapping; @@ -34,6 +35,8 @@ interface Props { validateForm: boolean; canUseInlineScripts: boolean; canUseStoredScripts: boolean; + rolesAPIClient: PublicMethodsOf; + docLinks: DocumentationLinksService; } interface State { @@ -163,6 +166,7 @@ export class MappingInfoPanel extends Component { > { defaultMessage="Create templates that describe the roles to assign to your users." />{' '} @@ -230,6 +234,7 @@ export class MappingInfoPanel extends Component { > { - return { - RolesApi: { - getRoles: () => Promise.resolve([{ name: 'foo_role' }, { name: 'bar role' }]), - }, - }; -}); +import { RolesAPIClient } from '../../../roles'; +import { rolesAPIClientMock } from '../../../roles/roles_api_client.mock'; describe('RoleSelector', () => { + let rolesAPI: PublicMethodsOf; + beforeEach(() => { + rolesAPI = rolesAPIClientMock.create(); + (rolesAPI as jest.Mocked).getRoles.mockResolvedValue([ + { name: 'foo_role' }, + { name: 'bar role' }, + ] as Role[]); + }); + it('allows roles to be selected, removing any previously selected role templates', () => { const props = { roleMapping: { @@ -36,6 +39,7 @@ describe('RoleSelector', () => { canUseInlineScripts: true, onChange: jest.fn(), mode: 'roles', + rolesAPIClient: rolesAPI, } as RoleSelector['props']; const wrapper = mountWithIntl(); @@ -57,6 +61,7 @@ describe('RoleSelector', () => { canUseInlineScripts: true, onChange: jest.fn(), mode: 'templates', + rolesAPIClient: rolesAPI, } as RoleSelector['props']; const wrapper = mountWithIntl(); @@ -87,6 +92,7 @@ describe('RoleSelector', () => { canUseInlineScripts: true, onChange: jest.fn(), mode: 'templates', + rolesAPIClient: rolesAPI, } as RoleSelector['props']; const wrapper = mountWithIntl(); @@ -122,6 +128,7 @@ describe('RoleSelector', () => { canUseInlineScripts: true, onChange: jest.fn(), mode: 'templates', + rolesAPIClient: rolesAPI, } as RoleSelector['props']; const wrapper = mountWithIntl(); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_selector.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_selector.tsx similarity index 94% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_selector.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_selector.tsx index 6b92d6b4907f16..992c2741ae93ee 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_selector.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_selector.tsx @@ -7,12 +7,13 @@ import React, { Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiComboBox, EuiFormRow, EuiHorizontalRule } from '@elastic/eui'; -import { RoleMapping, Role } from '../../../../../../../common/model'; -import { RolesApi } from '../../../../../../lib/roles_api'; +import { RoleMapping, Role } from '../../../../../common/model'; +import { RolesAPIClient } from '../../../roles'; import { AddRoleTemplateButton } from './add_role_template_button'; import { RoleTemplateEditor } from './role_template_editor'; interface Props { + rolesAPIClient: PublicMethodsOf; roleMapping: RoleMapping; canUseInlineScripts: boolean; canUseStoredScripts: boolean; @@ -32,7 +33,7 @@ export class RoleSelector extends React.Component { } public async componentDidMount() { - const roles = await RolesApi.getRoles(); + const roles = await this.props.rolesAPIClient.getRoles(); this.setState({ roles }); } diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_template_editor.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_template_editor.test.tsx similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_template_editor.test.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_template_editor.test.tsx diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_template_editor.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_template_editor.tsx similarity index 98% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_template_editor.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_template_editor.tsx index 4b8d34d2719960..d79651d7b9cd6d 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_template_editor.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_template_editor.tsx @@ -18,12 +18,12 @@ import { EuiSpacer, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { RoleTemplate } from '../../../../../../../common/model'; +import { RoleTemplate } from '../../../../../common/model'; import { isInlineRoleTemplate, isStoredRoleTemplate, isInvalidRoleTemplate, -} from '../../services/role_template_type'; +} from '../services/role_template_type'; import { RoleTemplateTypeSelect } from './role_template_type_select'; interface Props { diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_template_type_select.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_template_type_select.tsx similarity index 92% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_template_type_select.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_template_type_select.tsx index 4a06af0fb436ba..aa65c5c9bcae74 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_template_type_select.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_template_type_select.tsx @@ -7,8 +7,8 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiComboBox } from '@elastic/eui'; -import { RoleTemplate } from '../../../../../../../common/model'; -import { isInlineRoleTemplate, isStoredRoleTemplate } from '../../services/role_template_type'; +import { RoleTemplate } from '../../../../../common/model'; +import { isInlineRoleTemplate, isStoredRoleTemplate } from '../services/role_template_type'; const templateTypeOptions = [ { diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/_index.scss b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/_index.scss new file mode 100644 index 00000000000000..c3b2764e647130 --- /dev/null +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/_index.scss @@ -0,0 +1 @@ +@import './rule_editor_group'; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/_index.scss b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/_rule_editor_group.scss similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/_index.scss rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/_rule_editor_group.scss diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/add_rule_button.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/add_rule_button.test.tsx similarity index 97% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/add_rule_button.test.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/add_rule_button.test.tsx index 917b822acef3f2..d1411bd9bf2b97 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/add_rule_button.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/add_rule_button.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { AddRuleButton } from './add_rule_button'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { findTestSubject } from 'test_utils/find_test_subject'; -import { FieldRule, AllRule } from '../../../model'; +import { FieldRule, AllRule } from '../../model'; describe('AddRuleButton', () => { it('allows a field rule to be created', () => { diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/add_rule_button.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/add_rule_button.tsx similarity index 97% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/add_rule_button.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/add_rule_button.tsx index 100c0dd3eeaee4..9696fa337a74fc 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/add_rule_button.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/add_rule_button.tsx @@ -7,7 +7,7 @@ import React, { useState } from 'react'; import { EuiButtonEmpty, EuiPopover, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Rule, FieldRule, AllRule } from '../../../model'; +import { Rule, FieldRule, AllRule } from '../../model'; interface Props { onClick: (newRule: Rule) => void; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/field_rule_editor.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/field_rule_editor.test.tsx similarity index 99% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/field_rule_editor.test.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/field_rule_editor.test.tsx index 8d5d5c99ee99d7..5374f4625336de 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/field_rule_editor.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/field_rule_editor.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { FieldRuleEditor } from './field_rule_editor'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { FieldRule } from '../../../model'; +import { FieldRule } from '../../model'; import { findTestSubject } from 'test_utils/find_test_subject'; import { ReactWrapper } from 'enzyme'; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/field_rule_editor.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/field_rule_editor.tsx similarity index 99% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/field_rule_editor.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/field_rule_editor.tsx index 52cf70dbd12bd2..2506c18dcaf3a0 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/field_rule_editor.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/field_rule_editor.tsx @@ -18,7 +18,7 @@ import { EuiIcon, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FieldRule, FieldRuleValue } from '../../../model'; +import { FieldRule, FieldRuleValue } from '../../model'; interface Props { rule: FieldRule; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/index.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/index.tsx similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/index.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/index.tsx diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/json_rule_editor.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.test.tsx similarity index 89% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/json_rule_editor.test.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.test.tsx index 8a9b37ab0f4065..263c456e901cd9 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/json_rule_editor.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.test.tsx @@ -17,7 +17,10 @@ import { act } from 'react-dom/test-utils'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { JSONRuleEditor } from './json_rule_editor'; import { EuiCodeEditor } from '@elastic/eui'; -import { AllRule, AnyRule, FieldRule, ExceptAnyRule, ExceptAllRule } from '../../../model'; +import { AllRule, AnyRule, FieldRule, ExceptAnyRule, ExceptAllRule } from '../../model'; +import { DocumentationLinksService } from '../../documentation_links'; + +import { coreMock } from '../../../../../../../../src/core/public/mocks'; describe('JSONRuleEditor', () => { it('renders an empty rule set', () => { @@ -25,6 +28,7 @@ describe('JSONRuleEditor', () => { rules: null, onChange: jest.fn(), onValidityChange: jest.fn(), + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), }; const wrapper = mountWithIntl(); @@ -46,6 +50,7 @@ describe('JSONRuleEditor', () => { ]), onChange: jest.fn(), onValidityChange: jest.fn(), + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), }; const wrapper = mountWithIntl(); @@ -79,6 +84,7 @@ describe('JSONRuleEditor', () => { rules: null, onChange: jest.fn(), onValidityChange: jest.fn(), + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), }; const wrapper = mountWithIntl(); @@ -100,6 +106,7 @@ describe('JSONRuleEditor', () => { rules: null, onChange: jest.fn(), onValidityChange: jest.fn(), + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), }; const wrapper = mountWithIntl(); @@ -130,6 +137,7 @@ describe('JSONRuleEditor', () => { rules: null, onChange: jest.fn(), onValidityChange: jest.fn(), + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), }; const wrapper = mountWithIntl(); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/json_rule_editor.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.tsx similarity index 95% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/json_rule_editor.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.tsx index 371fb59f7a5d13..e7a9149513d208 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/json_rule_editor.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.tsx @@ -11,13 +11,14 @@ import 'brace/theme/github'; import { EuiCodeEditor, EuiFormRow, EuiButton, EuiSpacer, EuiLink, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { Rule, RuleBuilderError, generateRulesFromRaw } from '../../../model'; -import { documentationLinks } from '../../../services/documentation_links'; +import { DocumentationLinksService } from '../../documentation_links'; +import { Rule, RuleBuilderError, generateRulesFromRaw } from '../../model'; interface Props { rules: Rule | null; onChange: (updatedRules: Rule | null) => void; onValidityChange: (isValid: boolean) => void; + docLinks: DocumentationLinksService; } export const JSONRuleEditor = (props: Props) => { @@ -107,7 +108,7 @@ export const JSONRuleEditor = (props: Props) => { values={{ roleMappingAPI: ( diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_editor_panel.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.test.tsx similarity index 88% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_editor_panel.test.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.test.tsx index 809264183d30ca..b9c650cc1f77a4 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_editor_panel.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.test.tsx @@ -15,8 +15,11 @@ import { findTestSubject } from 'test_utils/find_test_subject'; // This is not required for the tests to pass, but it rather suppresses lengthy // warnings in the console which adds unnecessary noise to the test output. import 'test_utils/stub_web_worker'; -import { AllRule, FieldRule } from '../../../model'; +import { AllRule, FieldRule } from '../../model'; import { EuiErrorBoundary } from '@elastic/eui'; +import { DocumentationLinksService } from '../../documentation_links'; + +import { coreMock } from '../../../../../../../../src/core/public/mocks'; describe('RuleEditorPanel', () => { it('renders the visual editor when no rules are defined', () => { @@ -25,6 +28,7 @@ describe('RuleEditorPanel', () => { onChange: jest.fn(), onValidityChange: jest.fn(), validateForm: false, + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), }; const wrapper = mountWithIntl(); expect(wrapper.find(VisualRuleEditor)).toHaveLength(1); @@ -45,6 +49,7 @@ describe('RuleEditorPanel', () => { onChange: jest.fn(), onValidityChange: jest.fn(), validateForm: false, + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), }; const wrapper = mountWithIntl(); expect(wrapper.find(VisualRuleEditor)).toHaveLength(1); @@ -68,6 +73,7 @@ describe('RuleEditorPanel', () => { onChange: jest.fn(), onValidityChange: jest.fn(), validateForm: false, + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), }; const wrapper = mountWithIntl(); @@ -103,6 +109,7 @@ describe('RuleEditorPanel', () => { onChange: jest.fn(), onValidityChange: jest.fn(), validateForm: false, + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), }; const wrapper = mountWithIntl(); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_editor_panel.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.tsx similarity index 94% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_editor_panel.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.tsx index 4aab49b2b2efcf..6e6641caa1f399 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_editor_panel.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.tsx @@ -22,19 +22,20 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { RoleMapping } from '../../../../../../../common/model'; +import { RoleMapping } from '../../../../../common/model'; import { VisualRuleEditor } from './visual_rule_editor'; import { JSONRuleEditor } from './json_rule_editor'; -import { VISUAL_MAX_RULE_DEPTH } from '../../services/role_mapping_constants'; -import { Rule, generateRulesFromRaw } from '../../../model'; -import { validateRoleMappingRules } from '../../services/role_mapping_validation'; -import { documentationLinks } from '../../../services/documentation_links'; +import { VISUAL_MAX_RULE_DEPTH } from '../services/role_mapping_constants'; +import { Rule, generateRulesFromRaw } from '../../model'; +import { DocumentationLinksService } from '../../documentation_links'; +import { validateRoleMappingRules } from '../services/role_mapping_validation'; interface Props { rawRules: RoleMapping['rules']; onChange: (rawRules: RoleMapping['rules']) => void; onValidityChange: (isValid: boolean) => void; validateForm: boolean; + docLinks: DocumentationLinksService; } interface State { @@ -91,7 +92,7 @@ export class RuleEditorPanel extends Component { values={{ learnMoreLink: ( @@ -214,6 +215,7 @@ export class RuleEditorPanel extends Component { rules={this.state.rules} onChange={this.onRuleChange} onValidityChange={this.onValidityChange} + docLinks={this.props.docLinks} /> ); default: diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_editor.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_editor.test.tsx similarity index 99% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_editor.test.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_editor.test.tsx index 3e0e0e386e98c2..5946aac4306b1f 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_editor.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_editor.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { RuleGroupEditor } from './rule_group_editor'; import { shallowWithIntl, mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; -import { AllRule, FieldRule, AnyRule, ExceptAnyRule } from '../../../model'; +import { AllRule, FieldRule, AnyRule, ExceptAnyRule } from '../../model'; import { FieldRuleEditor } from './field_rule_editor'; import { AddRuleButton } from './add_rule_button'; import { EuiContextMenuItem } from '@elastic/eui'; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_editor.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_editor.tsx similarity index 97% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_editor.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_editor.tsx index 6fb33db179e8a6..c17a853a654676 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_editor.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_editor.tsx @@ -16,8 +16,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { AddRuleButton } from './add_rule_button'; import { RuleGroupTitle } from './rule_group_title'; import { FieldRuleEditor } from './field_rule_editor'; -import { RuleGroup, Rule, FieldRule } from '../../../model'; -import { isRuleGroup } from '../../services/is_rule_group'; +import { RuleGroup, Rule, FieldRule } from '../../model'; +import { isRuleGroup } from '../services/is_rule_group'; interface Props { rule: RuleGroup; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_title.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_title.tsx similarity index 97% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_title.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_title.tsx index e46893afd4d86e..6bef9c09eeef39 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_title.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_title.tsx @@ -15,14 +15,7 @@ import { EuiConfirmModal, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - RuleGroup, - AllRule, - AnyRule, - ExceptAllRule, - ExceptAnyRule, - FieldRule, -} from '../../../model'; +import { RuleGroup, AllRule, AnyRule, ExceptAllRule, ExceptAnyRule, FieldRule } from '../../model'; interface Props { rule: RuleGroup; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/visual_rule_editor.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/visual_rule_editor.test.tsx similarity index 99% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/visual_rule_editor.test.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/visual_rule_editor.test.tsx index 7c63613ee1cc9e..b40f0063acb08a 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/visual_rule_editor.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/visual_rule_editor.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { VisualRuleEditor } from './visual_rule_editor'; import { findTestSubject } from 'test_utils/find_test_subject'; -import { AnyRule, AllRule, FieldRule, ExceptAnyRule, ExceptAllRule } from '../../../model'; +import { AnyRule, AllRule, FieldRule, ExceptAnyRule, ExceptAllRule } from '../../model'; import { RuleGroupEditor } from './rule_group_editor'; import { FieldRuleEditor } from './field_rule_editor'; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/visual_rule_editor.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/visual_rule_editor.tsx similarity index 95% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/visual_rule_editor.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/visual_rule_editor.tsx index 214c583189fb80..2e3db275788ee4 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/visual_rule_editor.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/visual_rule_editor.tsx @@ -9,9 +9,9 @@ import { EuiEmptyPrompt, EuiCallOut, EuiSpacer, EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { FieldRuleEditor } from './field_rule_editor'; import { RuleGroupEditor } from './rule_group_editor'; -import { VISUAL_MAX_RULE_DEPTH } from '../../services/role_mapping_constants'; -import { Rule, FieldRule, RuleGroup, AllRule } from '../../../model'; -import { isRuleGroup } from '../../services/is_rule_group'; +import { VISUAL_MAX_RULE_DEPTH } from '../services/role_mapping_constants'; +import { Rule, FieldRule, RuleGroup, AllRule } from '../../model'; +import { isRuleGroup } from '../services/is_rule_group'; interface Props { rules: Rule | null; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/is_rule_group.ts b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/is_rule_group.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/is_rule_group.ts rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/is_rule_group.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_mapping_constants.ts b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_mapping_constants.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_mapping_constants.ts rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_mapping_constants.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_mapping_validation.test.ts b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_mapping_validation.test.ts similarity index 98% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_mapping_validation.test.ts rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_mapping_validation.test.ts index 9614c4338b631f..0c3f988ae6b105 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_mapping_validation.test.ts +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_mapping_validation.test.ts @@ -10,7 +10,7 @@ import { validateRoleMappingRules, validateRoleMappingForSave, } from './role_mapping_validation'; -import { RoleMapping } from '../../../../../../common/model'; +import { RoleMapping } from '../../../../../common/model'; describe('validateRoleMappingName', () => { it('requires a value', () => { diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_mapping_validation.ts b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_mapping_validation.ts similarity index 97% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_mapping_validation.ts rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_mapping_validation.ts index 5916d6fd9e1891..7695f1da14881a 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_mapping_validation.ts +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_mapping_validation.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { RoleMapping } from '../../../../../../common/model'; +import { RoleMapping } from '../../../../../common/model'; import { generateRulesFromRaw } from '../../model'; interface ValidationResult { diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_template_type.test.ts b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_template_type.test.ts similarity index 96% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_template_type.test.ts rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_template_type.test.ts index 8e1f47a4157ae9..c093bb1b3fbbc9 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_template_type.test.ts +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_template_type.test.ts @@ -9,7 +9,7 @@ import { isInlineRoleTemplate, isInvalidRoleTemplate, } from './role_template_type'; -import { RoleTemplate } from '../../../../../../common/model'; +import { RoleTemplate } from '../../../../../common/model'; describe('#isStoredRoleTemplate', () => { it('returns true for stored templates, false otherwise', () => { diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_template_type.ts b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_template_type.ts similarity index 96% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_template_type.ts rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_template_type.ts index 90d8d1a09e5877..5e646535a6c4d5 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_template_type.ts +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_template_type.ts @@ -9,7 +9,7 @@ import { StoredRoleTemplate, InlineRoleTemplate, InvalidRoleTemplate, -} from '../../../../../../common/model'; +} from '../../../../../common/model'; export function isStoredRoleTemplate( roleMappingTemplate: RoleTemplate diff --git a/x-pack/plugins/security/public/management/role_mappings/index.mock.ts b/x-pack/plugins/security/public/management/role_mappings/index.mock.ts new file mode 100644 index 00000000000000..826477a1a5b153 --- /dev/null +++ b/x-pack/plugins/security/public/management/role_mappings/index.mock.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 { roleMappingsAPIClientMock } from './role_mappings_api_client.mock'; diff --git a/x-pack/plugins/security/public/management/role_mappings/index.ts b/x-pack/plugins/security/public/management/role_mappings/index.ts new file mode 100644 index 00000000000000..f670ea61810386 --- /dev/null +++ b/x-pack/plugins/security/public/management/role_mappings/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 { roleMappingsManagementApp } from './role_mappings_management_app'; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/__snapshots__/rule_builder.test.ts.snap b/x-pack/plugins/security/public/management/role_mappings/model/__snapshots__/rule_builder.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/__snapshots__/rule_builder.test.ts.snap rename to x-pack/plugins/security/public/management/role_mappings/model/__snapshots__/rule_builder.test.ts.snap diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/all_rule.test.ts b/x-pack/plugins/security/public/management/role_mappings/model/all_rule.test.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/all_rule.test.ts rename to x-pack/plugins/security/public/management/role_mappings/model/all_rule.test.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/all_rule.ts b/x-pack/plugins/security/public/management/role_mappings/model/all_rule.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/all_rule.ts rename to x-pack/plugins/security/public/management/role_mappings/model/all_rule.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/any_rule.test.ts b/x-pack/plugins/security/public/management/role_mappings/model/any_rule.test.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/any_rule.test.ts rename to x-pack/plugins/security/public/management/role_mappings/model/any_rule.test.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/any_rule.ts b/x-pack/plugins/security/public/management/role_mappings/model/any_rule.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/any_rule.ts rename to x-pack/plugins/security/public/management/role_mappings/model/any_rule.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_all_rule.test.ts b/x-pack/plugins/security/public/management/role_mappings/model/except_all_rule.test.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_all_rule.test.ts rename to x-pack/plugins/security/public/management/role_mappings/model/except_all_rule.test.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_all_rule.ts b/x-pack/plugins/security/public/management/role_mappings/model/except_all_rule.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_all_rule.ts rename to x-pack/plugins/security/public/management/role_mappings/model/except_all_rule.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_any_rule.test.ts b/x-pack/plugins/security/public/management/role_mappings/model/except_any_rule.test.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_any_rule.test.ts rename to x-pack/plugins/security/public/management/role_mappings/model/except_any_rule.test.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_any_rule.ts b/x-pack/plugins/security/public/management/role_mappings/model/except_any_rule.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_any_rule.ts rename to x-pack/plugins/security/public/management/role_mappings/model/except_any_rule.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/field_rule.test.ts b/x-pack/plugins/security/public/management/role_mappings/model/field_rule.test.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/field_rule.test.ts rename to x-pack/plugins/security/public/management/role_mappings/model/field_rule.test.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/field_rule.ts b/x-pack/plugins/security/public/management/role_mappings/model/field_rule.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/field_rule.ts rename to x-pack/plugins/security/public/management/role_mappings/model/field_rule.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/index.ts b/x-pack/plugins/security/public/management/role_mappings/model/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/index.ts rename to x-pack/plugins/security/public/management/role_mappings/model/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule.ts b/x-pack/plugins/security/public/management/role_mappings/model/rule.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule.ts rename to x-pack/plugins/security/public/management/role_mappings/model/rule.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_builder.test.ts b/x-pack/plugins/security/public/management/role_mappings/model/rule_builder.test.ts similarity index 99% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_builder.test.ts rename to x-pack/plugins/security/public/management/role_mappings/model/rule_builder.test.ts index ebd48f6d15d99e..ad486a8b314c4f 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_builder.test.ts +++ b/x-pack/plugins/security/public/management/role_mappings/model/rule_builder.test.ts @@ -5,7 +5,7 @@ */ import { generateRulesFromRaw, FieldRule } from '.'; -import { RoleMapping } from '../../../../../common/model'; +import { RoleMapping } from '../../../../common/model'; import { RuleBuilderError } from './rule_builder_error'; describe('generateRulesFromRaw', () => { diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_builder.ts b/x-pack/plugins/security/public/management/role_mappings/model/rule_builder.ts similarity index 99% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_builder.ts rename to x-pack/plugins/security/public/management/role_mappings/model/rule_builder.ts index fe344b2ae38dd0..a384e61e521aba 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_builder.ts +++ b/x-pack/plugins/security/public/management/role_mappings/model/rule_builder.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { RoleMapping } from '../../../../../common/model'; +import { RoleMapping } from '../../../../common/model'; import { FieldRule, FieldRuleValue } from './field_rule'; import { AllRule } from './all_rule'; import { AnyRule } from './any_rule'; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_builder_error.ts b/x-pack/plugins/security/public/management/role_mappings/model/rule_builder_error.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_builder_error.ts rename to x-pack/plugins/security/public/management/role_mappings/model/rule_builder_error.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_group.ts b/x-pack/plugins/security/public/management/role_mappings/model/rule_group.ts similarity index 94% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_group.ts rename to x-pack/plugins/security/public/management/role_mappings/model/rule_group.ts index 3e1e7fad9b36f5..5077c79a543c46 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_group.ts +++ b/x-pack/plugins/security/public/management/role_mappings/model/rule_group.ts @@ -7,7 +7,7 @@ import { Rule } from './rule'; /** - * Represents a catagory of Role Mapping rules which are capable of containing other rules. + * Represents a category of Role Mapping rules which are capable of containing other rules. */ export abstract class RuleGroup extends Rule { /** diff --git a/x-pack/plugins/security/public/management/role_mappings/role_mappings_api_client.mock.ts b/x-pack/plugins/security/public/management/role_mappings/role_mappings_api_client.mock.ts new file mode 100644 index 00000000000000..07d583d1e983f6 --- /dev/null +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_api_client.mock.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. + */ + +export const roleMappingsAPIClientMock = { + create: () => ({ + checkRoleMappingFeatures: jest.fn(), + getRoleMappings: jest.fn(), + getRoleMapping: jest.fn(), + saveRoleMapping: jest.fn(), + deleteRoleMappings: jest.fn(), + }), +}; diff --git a/x-pack/legacy/plugins/security/public/lib/role_mappings_api.ts b/x-pack/plugins/security/public/management/role_mappings/role_mappings_api_client.ts similarity index 89% rename from x-pack/legacy/plugins/security/public/lib/role_mappings_api.ts rename to x-pack/plugins/security/public/management/role_mappings/role_mappings_api_client.ts index b8bcba91388b53..0a88ed1da9ac3a 100644 --- a/x-pack/legacy/plugins/security/public/lib/role_mappings_api.ts +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_api_client.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup } from 'src/core/public'; -import { RoleMapping } from '../../common/model'; +import { HttpStart } from 'src/core/public'; +import { RoleMapping } from '../../../common/model'; interface CheckRoleMappingFeaturesResponse { canManageRoleMappings: boolean; @@ -20,8 +20,8 @@ type DeleteRoleMappingsResponse = Array<{ error?: Error; }>; -export class RoleMappingsAPI { - constructor(private readonly http: CoreSetup['http']) {} +export class RoleMappingsAPIClient { + constructor(private readonly http: HttpStart) {} public async checkRoleMappingFeatures(): Promise { return this.http.get(`/internal/security/_check_role_mapping_features`); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/create_role_mapping_button/create_role_mapping_button.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/create_role_mapping_button/create_role_mapping_button.tsx similarity index 90% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/create_role_mapping_button/create_role_mapping_button.tsx rename to x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/create_role_mapping_button/create_role_mapping_button.tsx index 2342eeb97d03e3..6fe4bcc7a0bbb0 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/create_role_mapping_button/create_role_mapping_button.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/create_role_mapping_button/create_role_mapping_button.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { getCreateRoleMappingHref } from '../../../../management_urls'; +import { getCreateRoleMappingHref } from '../../../management_urls'; export const CreateRoleMappingButton = () => { return ( diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/create_role_mapping_button/index.ts b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/create_role_mapping_button/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/create_role_mapping_button/index.ts rename to x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/create_role_mapping_button/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/empty_prompt/empty_prompt.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/empty_prompt/empty_prompt.tsx similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/empty_prompt/empty_prompt.tsx rename to x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/empty_prompt/empty_prompt.tsx diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/empty_prompt/index.ts b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/empty_prompt/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/empty_prompt/index.ts rename to x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/empty_prompt/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/index.ts b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/index.ts rename to x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/index.ts diff --git a/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.test.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.test.tsx new file mode 100644 index 00000000000000..de0722b4cd85e2 --- /dev/null +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.test.tsx @@ -0,0 +1,219 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; +import { RoleMappingsGridPage } from '.'; +import { SectionLoading, PermissionDenied, NoCompatibleRealms } from '../components'; +import { EmptyPrompt } from './empty_prompt'; +import { findTestSubject } from 'test_utils/find_test_subject'; +import { EuiLink } from '@elastic/eui'; +import { act } from '@testing-library/react'; +import { DocumentationLinksService } from '../documentation_links'; + +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { roleMappingsAPIClientMock } from '../role_mappings_api_client.mock'; + +describe('RoleMappingsGridPage', () => { + it('renders an empty prompt when no role mappings exist', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.getRoleMappings.mockResolvedValue([]); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + expect(wrapper.find(SectionLoading)).toHaveLength(1); + expect(wrapper.find(EmptyPrompt)).toHaveLength(0); + + await nextTick(); + wrapper.update(); + + expect(wrapper.find(SectionLoading)).toHaveLength(0); + expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); + expect(wrapper.find(EmptyPrompt)).toHaveLength(1); + }); + + it('renders a permission denied message when unauthorized to manage role mappings', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: false, + hasCompatibleRealms: true, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + expect(wrapper.find(SectionLoading)).toHaveLength(1); + expect(wrapper.find(PermissionDenied)).toHaveLength(0); + + await nextTick(); + wrapper.update(); + + expect(wrapper.find(SectionLoading)).toHaveLength(0); + expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); + expect(wrapper.find(PermissionDenied)).toHaveLength(1); + }); + + it('renders a warning when there are no compatible realms enabled', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.getRoleMappings.mockResolvedValue([ + { + name: 'some realm', + enabled: true, + roles: [], + rules: { field: { username: '*' } }, + }, + ]); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: false, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + expect(wrapper.find(SectionLoading)).toHaveLength(1); + expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); + + await nextTick(); + wrapper.update(); + + expect(wrapper.find(SectionLoading)).toHaveLength(0); + expect(wrapper.find(NoCompatibleRealms)).toHaveLength(1); + }); + + it('renders links to mapped roles', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.getRoleMappings.mockResolvedValue([ + { + name: 'some realm', + enabled: true, + roles: ['superuser'], + rules: { field: { username: '*' } }, + }, + ]); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + await nextTick(); + wrapper.update(); + + const links = findTestSubject(wrapper, 'roleMappingRoles').find(EuiLink); + expect(links).toHaveLength(1); + expect(links.at(0).props()).toMatchObject({ + href: '#/management/security/roles/edit/superuser', + }); + }); + + it('describes the number of mapped role templates', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.getRoleMappings.mockResolvedValue([ + { + name: 'some realm', + enabled: true, + role_templates: [{}, {}], + rules: { field: { username: '*' } }, + }, + ]); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + await nextTick(); + wrapper.update(); + + const templates = findTestSubject(wrapper, 'roleMappingRoles'); + expect(templates).toHaveLength(1); + expect(templates.text()).toEqual(`2 role templates defined`); + }); + + it('allows role mappings to be deleted, refreshing the grid after', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.getRoleMappings.mockResolvedValue([ + { + name: 'some-realm', + enabled: true, + roles: ['superuser'], + rules: { field: { username: '*' } }, + }, + ]); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + }); + roleMappingsAPI.deleteRoleMappings.mockResolvedValue([ + { + name: 'some-realm', + success: true, + }, + ]); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + await nextTick(); + wrapper.update(); + + expect(roleMappingsAPI.getRoleMappings).toHaveBeenCalledTimes(1); + expect(roleMappingsAPI.deleteRoleMappings).not.toHaveBeenCalled(); + + findTestSubject(wrapper, `deleteRoleMappingButton-some-realm`).simulate('click'); + expect(findTestSubject(wrapper, 'deleteRoleMappingConfirmationModal')).toHaveLength(1); + + await act(async () => { + findTestSubject(wrapper, 'confirmModalConfirmButton').simulate('click'); + await nextTick(); + wrapper.update(); + }); + + expect(roleMappingsAPI.deleteRoleMappings).toHaveBeenCalledWith(['some-realm']); + // Expect an additional API call to refresh the grid + expect(roleMappingsAPI.getRoleMappings).toHaveBeenCalledTimes(2); + }); +}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/role_mappings_grid_page.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.tsx similarity index 93% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/role_mappings_grid_page.tsx rename to x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.tsx index 7b23f2288d1ba6..feb918cb6b301f 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/role_mappings_grid_page.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.tsx @@ -25,24 +25,27 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { RoleMapping } from '../../../../../../common/model'; -import { RoleMappingsAPI } from '../../../../../lib/role_mappings_api'; +import { NotificationsStart } from 'src/core/public'; +import { RoleMapping } from '../../../../common/model'; import { EmptyPrompt } from './empty_prompt'; import { NoCompatibleRealms, DeleteProvider, PermissionDenied, SectionLoading, -} from '../../components'; -import { documentationLinks } from '../../services/documentation_links'; +} from '../components'; import { getCreateRoleMappingHref, getEditRoleMappingHref, getEditRoleHref, -} from '../../../management_urls'; +} from '../../management_urls'; +import { DocumentationLinksService } from '../documentation_links'; +import { RoleMappingsAPIClient } from '../role_mappings_api_client'; interface Props { - roleMappingsAPI: RoleMappingsAPI; + roleMappingsAPI: PublicMethodsOf; + notifications: NotificationsStart; + docLinks: DocumentationLinksService; } interface State { @@ -140,7 +143,7 @@ export class RoleMappingsGridPage extends Component { values={{ learnMoreLink: ( @@ -168,7 +171,7 @@ export class RoleMappingsGridPage extends Component { {!this.state.hasCompatibleRealms && ( <> - + )} @@ -214,7 +217,10 @@ export class RoleMappingsGridPage extends Component { const search = { toolsLeft: selectedItems.length ? ( - + {deleteRoleMappingsPrompt => { return ( { return ( - + {deleteRoleMappingPrompt => { return ( ({ + RoleMappingsGridPage: (props: any) => `Role Mappings Page: ${JSON.stringify(props)}`, +})); + +jest.mock('./edit_role_mapping', () => ({ + EditRoleMappingPage: (props: any) => `Role Mapping Edit Page: ${JSON.stringify(props)}`, +})); + +import { roleMappingsManagementApp } from './role_mappings_management_app'; + +import { coreMock } from '../../../../../../src/core/public/mocks'; + +async function mountApp(basePath: string) { + const container = document.createElement('div'); + const setBreadcrumbs = jest.fn(); + + const unmount = await roleMappingsManagementApp + .create({ getStartServices: coreMock.createSetup().getStartServices as any }) + .mount({ basePath, element: container, setBreadcrumbs }); + + return { unmount, container, setBreadcrumbs }; +} + +describe('roleMappingsManagementApp', () => { + it('create() returns proper management app descriptor', () => { + expect( + roleMappingsManagementApp.create({ + getStartServices: coreMock.createSetup().getStartServices as any, + }) + ).toMatchInlineSnapshot(` + Object { + "id": "role_mappings", + "mount": [Function], + "order": 40, + "title": "Role Mappings", + } + `); + }); + + it('mount() works for the `grid` page', async () => { + const basePath = '/some-base-path/role_mappings'; + window.location.hash = basePath; + + const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: `#${basePath}`, text: 'Role Mappings' }]); + expect(container).toMatchInlineSnapshot(` +
+ Role Mappings Page: {"notifications":{"toasts":{}},"roleMappingsAPI":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"}} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('mount() works for the `create role mapping` page', async () => { + const basePath = '/some-base-path/role_mappings'; + window.location.hash = `${basePath}/edit`; + + const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `#${basePath}`, text: 'Role Mappings' }, + { text: 'Create' }, + ]); + expect(container).toMatchInlineSnapshot(` +
+ Role Mapping Edit Page: {"roleMappingsAPI":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"notifications":{"toasts":{}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"}} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('mount() works for the `edit role mapping` page', async () => { + const basePath = '/some-base-path/role_mappings'; + const roleMappingName = 'someRoleMappingName'; + window.location.hash = `${basePath}/edit/${roleMappingName}`; + + const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `#${basePath}`, text: 'Role Mappings' }, + { href: `#/some-base-path/role_mappings/edit/${roleMappingName}`, text: roleMappingName }, + ]); + expect(container).toMatchInlineSnapshot(` +
+ Role Mapping Edit Page: {"name":"someRoleMappingName","roleMappingsAPI":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"notifications":{"toasts":{}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"}} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('mount() properly encodes role mapping name in `edit role mapping` page link in breadcrumbs', async () => { + const basePath = '/some-base-path/role_mappings'; + const roleMappingName = 'some 安全性 role mapping'; + window.location.hash = `${basePath}/edit/${roleMappingName}`; + + const { setBreadcrumbs } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `#${basePath}`, text: 'Role Mappings' }, + { + href: + '#/some-base-path/role_mappings/edit/some%20%E5%AE%89%E5%85%A8%E6%80%A7%20role%20mapping', + text: roleMappingName, + }, + ]); + }); +}); diff --git a/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx new file mode 100644 index 00000000000000..af1572cedbadef --- /dev/null +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { HashRouter as Router, Route, Switch, useParams } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { CoreSetup } from 'src/core/public'; +import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; +import { PluginStartDependencies } from '../../plugin'; +import { RolesAPIClient } from '../roles'; +import { RoleMappingsAPIClient } from './role_mappings_api_client'; +import { DocumentationLinksService } from './documentation_links'; +import { RoleMappingsGridPage } from './role_mappings_grid'; +import { EditRoleMappingPage } from './edit_role_mapping'; + +interface CreateParams { + getStartServices: CoreSetup['getStartServices']; +} + +export const roleMappingsManagementApp = Object.freeze({ + id: 'role_mappings', + create({ getStartServices }: CreateParams) { + return { + id: this.id, + order: 40, + title: i18n.translate('xpack.security.management.roleMappingsTitle', { + defaultMessage: 'Role Mappings', + }), + async mount({ basePath, element, setBreadcrumbs }) { + const [{ docLinks, http, notifications, i18n: i18nStart }] = await getStartServices(); + const roleMappingsBreadcrumbs = [ + { + text: i18n.translate('xpack.security.roleMapping.breadcrumb', { + defaultMessage: 'Role Mappings', + }), + href: `#${basePath}`, + }, + ]; + + const roleMappingsAPIClient = new RoleMappingsAPIClient(http); + const dockLinksService = new DocumentationLinksService(docLinks); + const RoleMappingsGridPageWithBreadcrumbs = () => { + setBreadcrumbs(roleMappingsBreadcrumbs); + return ( + + ); + }; + + const EditRoleMappingsPageWithBreadcrumbs = () => { + const { name } = useParams<{ name?: string }>(); + + setBreadcrumbs([ + ...roleMappingsBreadcrumbs, + name + ? { text: name, href: `#${basePath}/edit/${encodeURIComponent(name)}` } + : { + text: i18n.translate('xpack.security.roleMappings.createBreadcrumb', { + defaultMessage: 'Create', + }), + }, + ]); + + return ( + + ); + }; + + render( + + + + + + + + + + + + , + element + ); + + return () => { + unmountComponentAtNode(element); + }; + }, + } as RegisterManagementAppArgs; + }, +}); diff --git a/x-pack/plugins/security/public/management/roles/_index.scss b/x-pack/plugins/security/public/management/roles/_index.scss new file mode 100644 index 00000000000000..5256c79f01f10f --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/_index.scss @@ -0,0 +1 @@ +@import './edit_role/index'; diff --git a/x-pack/plugins/security/public/management/roles/documentation_links.ts b/x-pack/plugins/security/public/management/roles/documentation_links.ts new file mode 100644 index 00000000000000..cf46973d3541ca --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/documentation_links.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 { DocLinksStart } from 'src/core/public'; + +export class DocumentationLinksService { + private readonly esDocBasePath: string; + + constructor(docLinks: DocLinksStart) { + this.esDocBasePath = `${docLinks.ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${docLinks.DOC_LINK_VERSION}/`; + } + + public getESClusterPrivilegesDocUrl() { + return `${this.esDocBasePath}security-privileges.html#privileges-list-cluster`; + } + + public getESRunAsPrivilegesDocUrl() { + return `${this.esDocBasePath}security-privileges.html#_run_as_privilege`; + } + + public getESIndicesPrivilegesDocUrl() { + return `${this.esDocBasePath}security-privileges.html#privileges-list-indices`; + } +} diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/lib/__snapshots__/validate_role.test.ts.snap b/x-pack/plugins/security/public/management/roles/edit_role/__snapshots__/validate_role.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/lib/__snapshots__/validate_role.test.ts.snap rename to x-pack/plugins/security/public/management/roles/edit_role/__snapshots__/validate_role.test.ts.snap diff --git a/x-pack/plugins/security/public/management/roles/edit_role/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/_index.scss new file mode 100644 index 00000000000000..0153b1734ceba0 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/_index.scss @@ -0,0 +1,3 @@ +@import './collapsible_panel/index'; +@import './spaces_popover_list/index'; +@import './privileges/index'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/__snapshots__/collapsible_panel.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/__snapshots__/collapsible_panel.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/__snapshots__/collapsible_panel.test.tsx.snap rename to x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/__snapshots__/collapsible_panel.test.tsx.snap diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/_collapsible_panel.scss b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/_collapsible_panel.scss similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/_collapsible_panel.scss rename to x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/_collapsible_panel.scss diff --git a/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/_index.scss new file mode 100644 index 00000000000000..c0f4f8ab9a8701 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/_index.scss @@ -0,0 +1 @@ +@import './collapsible_panel'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/collapsible_panel.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/collapsible_panel.test.tsx similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/collapsible_panel.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/collapsible_panel.test.tsx diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/collapsible_panel.tsx b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/collapsible_panel.tsx similarity index 99% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/collapsible_panel.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/collapsible_panel.tsx index 416dd7f6c4e5c7..01af7cb4509f64 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/collapsible_panel.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/collapsible_panel.tsx @@ -50,7 +50,6 @@ export class CollapsiblePanel extends Component { public getTitle = () => { return ( - // @ts-ignore diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/index.ts b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/index.ts rename to x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/delete_role_button.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/delete_role_button.test.tsx similarity index 94% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/delete_role_button.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/delete_role_button.test.tsx index cc16866c883555..f4af935be66487 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/delete_role_button.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/delete_role_button.test.tsx @@ -4,11 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiButtonEmpty, - // @ts-ignore - EuiConfirmModal, -} from '@elastic/eui'; +import { EuiButtonEmpty, EuiConfirmModal } from '@elastic/eui'; import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { DeleteRoleButton } from './delete_role_button'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/delete_role_button.tsx b/x-pack/plugins/security/public/management/roles/edit_role/delete_role_button.tsx similarity index 95% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/delete_role_button.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/delete_role_button.tsx index 1ae84d3fb72242..c6a10396f235c4 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/delete_role_button.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/delete_role_button.tsx @@ -4,13 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiButtonEmpty, - // @ts-ignore - EuiConfirmModal, - // @ts-ignore - EuiOverlayMask, -} from '@elastic/eui'; +import { EuiButtonEmpty, EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component, Fragment } from 'react'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx new file mode 100644 index 00000000000000..2b3d7c811f6de3 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx @@ -0,0 +1,552 @@ +/* + * 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 { ReactWrapper } from 'enzyme'; +import React from 'react'; +import { act } from '@testing-library/react'; +import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; +import { Capabilities } from 'src/core/public'; +import { Space } from '../../../../../spaces/common/model/space'; +import { Feature } from '../../../../../features/public'; +// These modules should be moved into a common directory +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { Actions } from '../../../../server/authorization/actions'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { privilegesFactory } from '../../../../server/authorization/privileges'; +import { Role } from '../../../../common/model'; +import { DocumentationLinksService } from '../documentation_links'; +import { EditRolePage } from './edit_role_page'; +import { SimplePrivilegeSection } from './privileges/kibana/simple_privilege_section'; +import { SpaceAwarePrivilegeSection } from './privileges/kibana/space_aware_privilege_section'; + +import { TransformErrorSection } from './privileges/kibana/transform_error_section'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; +import { licenseMock } from '../../../../common/licensing/index.mock'; +import { userAPIClientMock } from '../../users/index.mock'; +import { rolesAPIClientMock, indicesAPIClientMock, privilegesAPIClientMock } from '../index.mock'; + +const buildFeatures = () => { + return [ + { + id: 'feature1', + name: 'Feature 1', + icon: 'addDataApp', + app: ['feature1App'], + privileges: { + all: { + app: ['feature1App'], + ui: ['feature1-ui'], + savedObject: { + all: [], + read: [], + }, + }, + }, + }, + { + id: 'feature2', + name: 'Feature 2', + icon: 'addDataApp', + app: ['feature2App'], + privileges: { + all: { + app: ['feature2App'], + ui: ['feature2-ui'], + savedObject: { + all: ['feature2'], + read: ['config'], + }, + }, + }, + }, + ] as Feature[]; +}; + +const buildRawKibanaPrivileges = () => { + return privilegesFactory(new Actions('unit_test_version'), { + getFeatures: () => buildFeatures(), + }).get(); +}; + +const buildBuiltinESPrivileges = () => { + return { + cluster: ['all', 'manage', 'monitor'], + index: ['all', 'read', 'write', 'index'], + }; +}; + +const buildUICapabilities = (canManageSpaces = true) => { + return { + catalogue: {}, + management: {}, + navLinks: {}, + spaces: { + manage: canManageSpaces, + }, + } as Capabilities; +}; + +const buildSpaces = () => { + return [ + { + id: 'default', + name: 'Default', + disabledFeatures: [], + _reserved: true, + }, + { + id: 'space_1', + name: 'Space 1', + disabledFeatures: [], + }, + { + id: 'space_2', + name: 'Space 2', + disabledFeatures: ['feature2'], + }, + ] as Space[]; +}; + +const expectReadOnlyFormButtons = (wrapper: ReactWrapper) => { + expect(wrapper.find('button[data-test-subj="roleFormReturnButton"]')).toHaveLength(1); + expect(wrapper.find('button[data-test-subj="roleFormSaveButton"]')).toHaveLength(0); +}; + +const expectSaveFormButtons = (wrapper: ReactWrapper) => { + expect(wrapper.find('button[data-test-subj="roleFormReturnButton"]')).toHaveLength(0); + expect(wrapper.find('button[data-test-subj="roleFormSaveButton"]')).toHaveLength(1); +}; + +function getProps({ + action, + role, + canManageSpaces = true, + spacesEnabled = true, +}: { + action: 'edit' | 'clone'; + role?: Role; + canManageSpaces?: boolean; + spacesEnabled?: boolean; +}) { + const rolesAPIClient = rolesAPIClientMock.create(); + rolesAPIClient.getRole.mockResolvedValue(role); + + const indexPatterns = dataPluginMock.createStartContract().indexPatterns; + indexPatterns.getTitles = jest.fn().mockResolvedValue(['foo*', 'bar*']); + + const indicesAPIClient = indicesAPIClientMock.create(); + + const userAPIClient = userAPIClientMock.create(); + userAPIClient.getUsers.mockResolvedValue([]); + + const privilegesAPIClient = privilegesAPIClientMock.create(); + privilegesAPIClient.getAll.mockResolvedValue(buildRawKibanaPrivileges()); + privilegesAPIClient.getBuiltIn.mockResolvedValue(buildBuiltinESPrivileges()); + + const license = licenseMock.create(); + license.getFeatures.mockReturnValue({ + allowRoleDocumentLevelSecurity: true, + allowRoleFieldLevelSecurity: true, + } as any); + + const { fatalErrors } = coreMock.createSetup(); + const { http, docLinks, notifications } = coreMock.createStart(); + http.get.mockImplementation(async path => { + if (path === '/api/features') { + return buildFeatures(); + } + + if (path === '/api/spaces/space') { + return buildSpaces(); + } + }); + + return { + action, + roleName: role?.name, + license, + http, + indexPatterns, + indicesAPIClient, + privilegesAPIClient, + rolesAPIClient, + userAPIClient, + notifications, + docLinks: new DocumentationLinksService(docLinks), + fatalErrors, + spacesEnabled, + uiCapabilities: buildUICapabilities(canManageSpaces), + }; +} + +describe('', () => { + describe('with spaces enabled', () => { + it('can render a reserved role', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(1); + expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); + expectReadOnlyFormButtons(wrapper); + }); + + it('can render a user defined role', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(0); + expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); + expectSaveFormButtons(wrapper); + }); + + it('can render when creating a new role', async () => { + const wrapper = mountWithIntl(); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); + expectSaveFormButtons(wrapper); + }); + + it('can render when cloning an existing role', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); + expectSaveFormButtons(wrapper); + }); + + it('renders an auth error when not authorized to manage spaces', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(0); + + expect( + wrapper.find('EuiCallOut[data-test-subj="userCannotManageSpacesCallout"]') + ).toHaveLength(1); + + expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); + expectSaveFormButtons(wrapper); + }); + + it('renders a partial read-only view when there is a transform error', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(TransformErrorSection)).toHaveLength(1); + expectReadOnlyFormButtons(wrapper); + }); + }); + + describe('with spaces disabled', () => { + it('can render a reserved role', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(1); + expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); + expectReadOnlyFormButtons(wrapper); + }); + + it('can render a user defined role', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(0); + expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); + expectSaveFormButtons(wrapper); + }); + + it('can render when creating a new role', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); + expectSaveFormButtons(wrapper); + }); + + it('can render when cloning an existing role', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); + expectSaveFormButtons(wrapper); + }); + + it('does not care if user cannot manage spaces', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(0); + + expect( + wrapper.find('EuiCallOut[data-test-subj="userCannotManageSpacesCallout"]') + ).toHaveLength(0); + + expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); + expectSaveFormButtons(wrapper); + }); + + it('renders a partial read-only view when there is a transform error', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(TransformErrorSection)).toHaveLength(1); + expectReadOnlyFormButtons(wrapper); + }); + }); + + it('can render if features are not available', async () => { + const { http } = coreMock.createStart(); + http.get.mockImplementation(async path => { + if (path === '/api/features') { + const error = { response: { status: 404 } }; + throw error; + } + + if (path === '/api/spaces/space') { + return buildSpaces(); + } + }); + + const wrapper = mountWithIntl(); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); + expectSaveFormButtons(wrapper); + }); + + it('can render if index patterns are not available', async () => { + const indexPatterns = dataPluginMock.createStartContract().indexPatterns; + indexPatterns.getTitles = jest.fn().mockRejectedValue({ response: { status: 403 } }); + + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); + expectSaveFormButtons(wrapper); + }); +}); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx new file mode 100644 index 00000000000000..33e69a68ca8960 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx @@ -0,0 +1,594 @@ +/* + * 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 _, { get } from 'lodash'; +import { + EuiButton, + EuiButtonEmpty, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiPanel, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { + ChangeEvent, + Fragment, + FunctionComponent, + HTMLProps, + useEffect, + useRef, + useState, +} from 'react'; +import { + Capabilities, + FatalErrorsSetup, + HttpStart, + IHttpFetchError, + NotificationsStart, +} from 'src/core/public'; +import { IndexPatternsContract } from '../../../../../../../src/plugins/data/public'; +import { Space } from '../../../../../spaces/common/model/space'; +import { Feature } from '../../../../../features/public'; +import { + KibanaPrivileges, + RawKibanaPrivileges, + Role, + BuiltinESPrivileges, + isReadOnlyRole as checkIfRoleReadOnly, + isReservedRole as checkIfRoleReserved, + copyRole, + prepareRoleClone, + RoleIndexPrivilege, +} from '../../../../common/model'; +import { ROLES_PATH } from '../../management_urls'; +import { RoleValidationResult, RoleValidator } from './validate_role'; +import { DeleteRoleButton } from './delete_role_button'; +import { ElasticsearchPrivileges, KibanaPrivilegesRegion } from './privileges'; +import { ReservedRoleBadge } from './reserved_role_badge'; +import { SecurityLicense } from '../../../../common/licensing'; +import { UserAPIClient } from '../../users'; +import { DocumentationLinksService } from '../documentation_links'; +import { IndicesAPIClient } from '../indices_api_client'; +import { RolesAPIClient } from '../roles_api_client'; +import { PrivilegesAPIClient } from '../privileges_api_client'; + +interface Props { + action: 'edit' | 'clone'; + roleName?: string; + indexPatterns: IndexPatternsContract; + userAPIClient: PublicMethodsOf; + indicesAPIClient: PublicMethodsOf; + rolesAPIClient: PublicMethodsOf; + privilegesAPIClient: PublicMethodsOf; + docLinks: DocumentationLinksService; + http: HttpStart; + license: SecurityLicense; + spacesEnabled: boolean; + uiCapabilities: Capabilities; + notifications: NotificationsStart; + fatalErrors: FatalErrorsSetup; +} + +function useRunAsUsers( + userAPIClient: PublicMethodsOf, + fatalErrors: FatalErrorsSetup +) { + const [userNames, setUserNames] = useState(null); + useEffect(() => { + userAPIClient.getUsers().then( + users => setUserNames(users.map(user => user.username)), + err => fatalErrors.add(err) + ); + }, [fatalErrors, userAPIClient]); + + return userNames; +} + +function useIndexPatternsTitles( + indexPatterns: IndexPatternsContract, + fatalErrors: FatalErrorsSetup, + notifications: NotificationsStart +) { + const [indexPatternsTitles, setIndexPatternsTitles] = useState(null); + useEffect(() => { + indexPatterns + .getTitles() + .catch((err: IHttpFetchError) => { + // If user doesn't have access to the index patterns they still should be able to create new + // or edit existing role. + if (err.response?.status === 403) { + notifications.toasts.addDanger({ + title: i18n.translate('xpack.security.management.roles.noIndexPatternsPermission', { + defaultMessage: 'You need permission to access the list of available index patterns.', + }), + }); + return []; + } + + fatalErrors.add(err); + throw err; + }) + .then(setIndexPatternsTitles); + }, [fatalErrors, indexPatterns, notifications]); + + return indexPatternsTitles; +} + +function usePrivileges( + privilegesAPIClient: PublicMethodsOf, + fatalErrors: FatalErrorsSetup +) { + const [privileges, setPrivileges] = useState<[RawKibanaPrivileges, BuiltinESPrivileges] | null>( + null + ); + useEffect(() => { + Promise.all([ + privilegesAPIClient.getAll({ includeActions: true }), + privilegesAPIClient.getBuiltIn(), + ]).then( + ([kibanaPrivileges, builtInESPrivileges]) => + setPrivileges([kibanaPrivileges, builtInESPrivileges]), + err => fatalErrors.add(err) + ); + }, [privilegesAPIClient, fatalErrors]); + + return privileges; +} + +function useRole( + rolesAPIClient: PublicMethodsOf, + fatalErrors: FatalErrorsSetup, + notifications: NotificationsStart, + license: SecurityLicense, + action: string, + roleName?: string +) { + const [role, setRole] = useState(null); + useEffect(() => { + const rolePromise = roleName + ? rolesAPIClient.getRole(roleName) + : Promise.resolve({ + name: '', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [], + _unrecognized_applications: [], + } as Role); + + rolePromise + .then(fetchedRole => { + if (action === 'clone' && checkIfRoleReserved(fetchedRole)) { + backToRoleList(); + return; + } + + if (fetchedRole.elasticsearch.indices.length === 0) { + const emptyOption: RoleIndexPrivilege = { + names: [], + privileges: [], + }; + + const { + allowRoleDocumentLevelSecurity, + allowRoleFieldLevelSecurity, + } = license.getFeatures(); + + if (allowRoleFieldLevelSecurity) { + emptyOption.field_security = { + grant: ['*'], + except: [], + }; + } + + if (allowRoleDocumentLevelSecurity) { + emptyOption.query = ''; + } + + fetchedRole.elasticsearch.indices.push(emptyOption); + } + + setRole(action === 'clone' ? prepareRoleClone(fetchedRole) : copyRole(fetchedRole)); + }) + .catch((err: IHttpFetchError) => { + if (err.response?.status === 404) { + notifications.toasts.addDanger({ + title: i18n.translate('xpack.security.management.roles.roleNotFound', { + defaultMessage: 'No "{roleName}" role found.', + values: { roleName }, + }), + }); + backToRoleList(); + } else { + fatalErrors.add(err); + } + }); + }, [roleName, action, fatalErrors, rolesAPIClient, notifications, license]); + + return [role, setRole] as [Role | null, typeof setRole]; +} + +function useSpaces(http: HttpStart, fatalErrors: FatalErrorsSetup, spacesEnabled: boolean) { + const [spaces, setSpaces] = useState(null); + useEffect(() => { + (spacesEnabled ? http.get('/api/spaces/space') : Promise.resolve([])).then( + fetchedSpaces => setSpaces(fetchedSpaces), + err => fatalErrors.add(err) + ); + }, [http, fatalErrors, spacesEnabled]); + + return spaces; +} + +function useFeatures(http: HttpStart, fatalErrors: FatalErrorsSetup) { + const [features, setFeatures] = useState(null); + useEffect(() => { + http + .get('/api/features') + .catch((err: IHttpFetchError) => { + // Currently, the `/api/features` endpoint effectively requires the "Global All" kibana privilege (e.g., what + // the `kibana_user` grants), because it returns information about all registered features (#35841). It's + // possible that a user with `manage_security` will attempt to visit the role management page without the + // correct Kibana privileges. If that's the case, then they receive a partial view of the role, and the UI does + // not allow them to make changes to that role's kibana privileges. When this user visits the edit role page, + // this API endpoint will throw a 404, which causes view to fail completely. So we instead attempt to detect the + // 404 here, and respond in a way that still allows the UI to render itself. + const unauthorizedForFeatures = err.response?.status === 404; + if (unauthorizedForFeatures) { + return []; + } + + fatalErrors.add(err); + throw err; + }) + .then(setFeatures); + }, [http, fatalErrors]); + + return features; +} + +function backToRoleList() { + window.location.hash = ROLES_PATH; +} + +export const EditRolePage: FunctionComponent = ({ + userAPIClient, + indexPatterns, + rolesAPIClient, + indicesAPIClient, + privilegesAPIClient, + http, + roleName, + action, + fatalErrors, + spacesEnabled, + license, + docLinks, + uiCapabilities, + notifications, +}) => { + // We should keep the same mutable instance of Validator for every re-render since we'll + // eventually enable validation after the first time user tries to save a role. + const { current: validator } = useRef(new RoleValidator({ shouldValidate: false })); + + const [formError, setFormError] = useState(null); + const runAsUsers = useRunAsUsers(userAPIClient, fatalErrors); + const indexPatternsTitles = useIndexPatternsTitles(indexPatterns, fatalErrors, notifications); + const privileges = usePrivileges(privilegesAPIClient, fatalErrors); + const spaces = useSpaces(http, fatalErrors, spacesEnabled); + const features = useFeatures(http, fatalErrors); + const [role, setRole] = useRole( + rolesAPIClient, + fatalErrors, + notifications, + license, + action, + roleName + ); + + if (!role || !runAsUsers || !indexPatternsTitles || !privileges || !spaces || !features) { + return null; + } + + const isEditingExistingRole = !!roleName && action === 'edit'; + const isReadOnlyRole = checkIfRoleReadOnly(role); + const isReservedRole = checkIfRoleReserved(role); + + const [kibanaPrivileges, builtInESPrivileges] = privileges; + + const getFormTitle = () => { + let titleText; + const props: HTMLProps = { + tabIndex: 0, + }; + if (isReservedRole) { + titleText = ( + + ); + props['aria-describedby'] = 'reservedRoleDescription'; + } else if (isEditingExistingRole) { + titleText = ( + + ); + } else { + titleText = ( + + ); + } + + return ( + +

+ {titleText} +

+
+ ); + }; + + const getActionButton = () => { + if (isEditingExistingRole && !isReadOnlyRole) { + return ( + + + + ); + } + + return null; + }; + + const getRoleName = () => { + return ( + + + } + helpText={ + !isReservedRole && isEditingExistingRole ? ( + + ) : ( + undefined + ) + } + {...validator.validateRoleName(role)} + > + + + + ); + }; + + const onNameChange = (e: ChangeEvent) => + setRole({ + ...role, + name: e.target.value.replace(/\s/g, '_'), + }); + + const getElasticsearchPrivileges = () => { + return ( +
+ + +
+ ); + }; + + const onRoleChange = (newRole: Role) => setRole(newRole); + + const getKibanaPrivileges = () => { + return ( +
+ + +
+ ); + }; + + const getFormButtons = () => { + if (isReadOnlyRole) { + return getReturnToRoleListButton(); + } + + return ( + + {getSaveButton()} + {getCancelButton()} + + {getActionButton()} + + ); + }; + + const getReturnToRoleListButton = () => { + return ( + + + + ); + }; + + const getSaveButton = () => { + const saveText = isEditingExistingRole ? ( + + ) : ( + + ); + + return ( + + {saveText} + + ); + }; + + const getCancelButton = () => { + return ( + + + + ); + }; + + const saveRole = async () => { + validator.enableValidation(); + + const result = validator.validateForSave(role); + if (result.isInvalid) { + setFormError(result); + } else { + setFormError(null); + + try { + await rolesAPIClient.saveRole({ role, spacesEnabled }); + } catch (error) { + notifications.toasts.addDanger(get(error, 'data.message')); + return; + } + + notifications.toasts.addSuccess( + i18n.translate( + 'xpack.security.management.editRole.roleSuccessfullySavedNotificationMessage', + { defaultMessage: 'Saved role' } + ) + ); + + backToRoleList(); + } + }; + + const handleDeleteRole = async () => { + try { + await rolesAPIClient.deleteRole(role.name); + } catch (error) { + notifications.toasts.addDanger(get(error, 'data.message')); + return; + } + + notifications.toasts.addSuccess( + i18n.translate( + 'xpack.security.management.editRole.roleSuccessfullyDeletedNotificationMessage', + { defaultMessage: 'Deleted role' } + ) + ); + + backToRoleList(); + }; + + const description = spacesEnabled ? ( + + ) : ( + + ); + + return ( +
+ + {getFormTitle()} + + + + {description} + + {isReservedRole && ( + + + +

+ +

+
+
+ )} + + + + {getRoleName()} + + {getElasticsearchPrivileges()} + + {getKibanaPrivileges()} + + + + {getFormButtons()} +
+
+ ); +}; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/index.ts b/x-pack/plugins/security/public/management/roles/edit_role/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/index.ts rename to x-pack/plugins/security/public/management/roles/edit_role/index.ts diff --git a/x-pack/legacy/plugins/security/public/lib/privilege_utils.test.ts b/x-pack/plugins/security/public/management/roles/edit_role/privilege_utils.test.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/lib/privilege_utils.test.ts rename to x-pack/plugins/security/public/management/roles/edit_role/privilege_utils.test.ts diff --git a/x-pack/legacy/plugins/security/public/lib/privilege_utils.ts b/x-pack/plugins/security/public/management/roles/edit_role/privilege_utils.ts similarity index 93% rename from x-pack/legacy/plugins/security/public/lib/privilege_utils.ts rename to x-pack/plugins/security/public/management/roles/edit_role/privilege_utils.ts index 74bde71dc421ab..3fd8536951967a 100644 --- a/x-pack/legacy/plugins/security/public/lib/privilege_utils.ts +++ b/x-pack/plugins/security/public/management/roles/edit_role/privilege_utils.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RoleKibanaPrivilege } from '../../common/model'; +import { RoleKibanaPrivilege } from '../../../../common/model'; /** * Determines if the passed privilege spec defines global privileges. diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/_index.scss new file mode 100644 index 00000000000000..a1a9d038065e61 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/_index.scss @@ -0,0 +1,2 @@ +@import './privilege_feature_icon'; +@import './kibana/index'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/_privilege_feature_icon.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/_privilege_feature_icon.scss new file mode 100644 index 00000000000000..a7f24c96a28216 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/_privilege_feature_icon.scss @@ -0,0 +1,4 @@ +.secPrivilegeFeatureIcon { + flex-shrink: 0; + margin-right: $euiSizeS; +} diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/__snapshots__/cluster_privileges.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/cluster_privileges.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/__snapshots__/cluster_privileges.test.tsx.snap rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/cluster_privileges.test.tsx.snap diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap similarity index 87% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap index 795131337c31fd..323629de7578d7 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap @@ -15,7 +15,7 @@ exports[`it renders without crashing 1`] = ` /> { + expect(shallowWithIntl()).toMatchSnapshot(); +}); + +test('it renders ClusterPrivileges', () => { + expect( + mountWithIntl().find(ClusterPrivileges) + ).toHaveLength(1); +}); + +test('it renders IndexPrivileges', () => { + expect( + mountWithIntl().find(IndexPrivileges) + ).toHaveLength(1); +}); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/elasticsearch_privileges.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.tsx similarity index 89% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/elasticsearch_privileges.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.tsx index c0e6db3fef21c8..96249ccf3ff87e 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/elasticsearch_privileges.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.tsx @@ -7,7 +7,6 @@ import { EuiButton, EuiComboBox, - // @ts-ignore EuiDescribedFormGroup, EuiFormRow, EuiHorizontalRule, @@ -19,26 +18,26 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component, Fragment } from 'react'; -import { Role, BuiltinESPrivileges } from '../../../../../../../common/model'; -// @ts-ignore -import { documentationLinks } from '../../../../../../documentation_links'; -import { RoleValidator } from '../../../lib/validate_role'; +import { Role, BuiltinESPrivileges } from '../../../../../../common/model'; +import { SecurityLicense } from '../../../../../../common/licensing'; +import { IndicesAPIClient } from '../../../indices_api_client'; +import { RoleValidator } from '../../validate_role'; import { CollapsiblePanel } from '../../collapsible_panel'; import { ClusterPrivileges } from './cluster_privileges'; - import { IndexPrivileges } from './index_privileges'; +import { DocumentationLinksService } from '../../../documentation_links'; interface Props { role: Role; editable: boolean; - httpClient: any; + indicesAPIClient: PublicMethodsOf; + docLinks: DocumentationLinksService; + license: SecurityLicense; onChange: (role: Role) => void; runAsUsers: string[]; validator: RoleValidator; builtinESPrivileges: BuiltinESPrivileges; indexPatterns: string[]; - allowDocumentLevelSecurity: boolean; - allowFieldLevelSecurity: boolean; } export class ElasticsearchPrivileges extends Component { @@ -53,23 +52,22 @@ export class ElasticsearchPrivileges extends Component { public getForm = () => { const { role, - httpClient, + indicesAPIClient, + docLinks, validator, onChange, editable, indexPatterns, - allowDocumentLevelSecurity, - allowFieldLevelSecurity, + license, builtinESPrivileges, } = this.props; const indexProps = { role, - httpClient, + indicesAPIClient, validator, indexPatterns, - allowDocumentLevelSecurity, - allowFieldLevelSecurity, + license, onChange, availableIndexPrivileges: builtinESPrivileges.index, }; @@ -91,7 +89,7 @@ export class ElasticsearchPrivileges extends Component { id="xpack.security.management.editRole.elasticSearchPrivileges.manageRoleActionsDescription" defaultMessage="Manage the actions this role can perform against your cluster. " /> - {this.learnMore(documentationLinks.esClusterPrivileges)} + {this.learnMore(docLinks.getESClusterPrivilegesDocUrl())}

} > @@ -121,7 +119,7 @@ export class ElasticsearchPrivileges extends Component { id="xpack.security.management.editRole.elasticSearchPrivileges.howToBeSubmittedOnBehalfOfOtherUsersDescription" defaultMessage="Allow requests to be submitted on the behalf of other users. " /> - {this.learnMore(documentationLinks.esRunAsPrivileges)} + {this.learnMore(docLinks.getESRunAsPrivilegesDocUrl())}

} > @@ -165,7 +163,7 @@ export class ElasticsearchPrivileges extends Component { id="xpack.security.management.editRole.elasticSearchPrivileges.controlAccessToClusterDataDescription" defaultMessage="Control access to the data in your cluster. " /> - {this.learnMore(documentationLinks.esIndicesPrivileges)} + {this.learnMore(docLinks.getESIndicesPrivilegesDocUrl())}

diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privilege_form.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.test.tsx similarity index 99% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privilege_form.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.test.tsx index 6d386fd78a11b1..5e2da513143653 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privilege_form.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.test.tsx @@ -6,7 +6,7 @@ import { EuiButtonIcon, EuiTextArea } from '@elastic/eui'; import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { RoleValidator } from '../../../lib/validate_role'; +import { RoleValidator } from '../../validate_role'; import { IndexPrivilegeForm } from './index_privilege_form'; test('it renders without crashing', () => { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privilege_form.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.tsx similarity index 98% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privilege_form.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.tsx index bafc56dc167ea5..15e0367c2b6dc1 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privilege_form.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.tsx @@ -19,8 +19,8 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import _ from 'lodash'; import React, { ChangeEvent, Component, Fragment } from 'react'; -import { RoleIndexPrivilege } from '../../../../../../../common/model'; -import { RoleValidator } from '../../../lib/validate_role'; +import { RoleIndexPrivilege } from '../../../../../../common/model'; +import { RoleValidator } from '../../validate_role'; const fromOption = (option: any) => option.label; const toOption = (value: string) => ({ label: value }); @@ -164,7 +164,6 @@ export class IndexPrivilegeForm extends Component { {!isReadOnlyRole && ( { - // @ts-ignore missing "compressed" prop definition { } return ( - // @ts-ignore {!this.props.isReadOnlyRole && ( { - // @ts-ignore missing "compressed" proptype new Promise(setImmediate); test('it renders without crashing', async () => { + const license = licenseMock.create(); + license.getFeatures.mockReturnValue({ + allowRoleFieldLevelSecurity: true, + allowRoleDocumentLevelSecurity: true, + } as any); + const props = { role: { name: '', @@ -25,14 +34,13 @@ test('it renders without crashing', async () => { run_as: [], }, }, - httpClient: jest.fn(), onChange: jest.fn(), indexPatterns: [], editable: true, - allowDocumentLevelSecurity: true, - allowFieldLevelSecurity: true, validator: new RoleValidator(), availableIndexPrivileges: ['all', 'read', 'write', 'index'], + indicesAPIClient: indicesAPIClientMock.create(), + license, }; const wrapper = shallowWithIntl(); await flushPromises(); @@ -40,6 +48,12 @@ test('it renders without crashing', async () => { }); test('it renders a IndexPrivilegeForm for each privilege on the role', async () => { + const license = licenseMock.create(); + license.getFeatures.mockReturnValue({ + allowRoleFieldLevelSecurity: true, + allowRoleDocumentLevelSecurity: true, + } as any); + const props = { role: { name: '', @@ -59,14 +73,13 @@ test('it renders a IndexPrivilegeForm for each privilege on the role', async () run_as: [], }, }, - httpClient: jest.fn(), onChange: jest.fn(), indexPatterns: [], editable: true, - allowDocumentLevelSecurity: true, - allowFieldLevelSecurity: true, validator: new RoleValidator(), availableIndexPrivileges: ['all', 'read', 'write', 'index'], + indicesAPIClient: indicesAPIClientMock.create(), + license, }; const wrapper = mountWithIntl(); await flushPromises(); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privileges.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.tsx similarity index 84% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privileges.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.tsx index f09084ad2bb382..2c745067fede2d 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privileges.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.tsx @@ -5,19 +5,23 @@ */ import _ from 'lodash'; import React, { Component, Fragment } from 'react'; -import { Role, RoleIndexPrivilege } from '../../../../../../../common/model'; -import { isReadOnlyRole, isRoleEnabled } from '../../../../../../lib/role_utils'; -import { getFields } from '../../../../../../objects'; -import { RoleValidator } from '../../../lib/validate_role'; +import { + Role, + RoleIndexPrivilege, + isReadOnlyRole, + isRoleEnabled, +} from '../../../../../../common/model'; +import { SecurityLicense } from '../../../../../../common/licensing'; +import { IndicesAPIClient } from '../../../indices_api_client'; +import { RoleValidator } from '../../validate_role'; import { IndexPrivilegeForm } from './index_privilege_form'; interface Props { role: Role; indexPatterns: string[]; availableIndexPrivileges: string[]; - allowDocumentLevelSecurity: boolean; - allowFieldLevelSecurity: boolean; - httpClient: any; + indicesAPIClient: PublicMethodsOf; + license: SecurityLicense; onChange: (role: Role) => void; validator: RoleValidator; } @@ -43,20 +47,16 @@ export class IndexPrivileges extends Component { public render() { const { indices = [] } = this.props.role.elasticsearch; - const { - indexPatterns, - allowDocumentLevelSecurity, - allowFieldLevelSecurity, - availableIndexPrivileges, - } = this.props; + const { indexPatterns, license, availableIndexPrivileges } = this.props; + const { allowRoleDocumentLevelSecurity, allowRoleFieldLevelSecurity } = license.getFeatures(); const props = { indexPatterns, // If editing an existing role while that has been disabled, always show the FLS/DLS fields because currently // a role is only marked as disabled if it has FLS/DLS setup (usually before the user changed to a license that // doesn't permit FLS/DLS). - allowDocumentLevelSecurity: allowDocumentLevelSecurity || !isRoleEnabled(this.props.role), - allowFieldLevelSecurity: allowFieldLevelSecurity || !isRoleEnabled(this.props.role), + allowDocumentLevelSecurity: allowRoleDocumentLevelSecurity || !isRoleEnabled(this.props.role), + allowFieldLevelSecurity: allowRoleFieldLevelSecurity || !isRoleEnabled(this.props.role), isReadOnlyRole: isReadOnlyRole(this.props.role), }; @@ -171,7 +171,7 @@ export class IndexPrivileges extends Component { try { return { - [pattern]: await getFields(this.props.httpClient, pattern), + [pattern]: await this.props.indicesAPIClient.getFields(pattern), }; } catch (e) { return { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/index.ts b/x-pack/plugins/security/public/management/roles/edit_role/privileges/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/index.ts rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/kibana_privileges_region.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/__snapshots__/kibana_privileges_region.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/kibana_privileges_region.test.tsx.snap rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/__snapshots__/kibana_privileges_region.test.tsx.snap diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/_index.scss new file mode 100644 index 00000000000000..19547c0e1953e0 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/_index.scss @@ -0,0 +1,2 @@ +@import './feature_table/index'; +@import './space_aware_privilege_section/index'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/lib/constants.ts b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/constants.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/lib/constants.ts rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/constants.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/__snapshots__/feature_table.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/__snapshots__/feature_table.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/__snapshots__/feature_table.test.tsx.snap rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/__snapshots__/feature_table.test.tsx.snap diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/_change_all_privileges.scss similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/_index.scss rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/_change_all_privileges.scss diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/_index.scss new file mode 100644 index 00000000000000..6a96553742819f --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/_index.scss @@ -0,0 +1 @@ +@import './change_all_privileges'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/change_all_privileges.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/change_all_privileges.tsx similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/change_all_privileges.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/change_all_privileges.tsx diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.test.tsx similarity index 94% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.test.tsx index 9648bf1d111bf4..dea42e16f99d42 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.test.tsx @@ -3,12 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore import { EuiInMemoryTable } from '@elastic/eui'; import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { FeaturesPrivileges, KibanaPrivileges, Role } from '../../../../../../../../common/model'; -import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator'; +import { FeaturesPrivileges, KibanaPrivileges, Role } from '../../../../../../../common/model'; +import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator'; import { FeatureTable } from './feature_table'; const defaultPrivilegeDefinition = new KibanaPrivileges({ @@ -113,7 +112,6 @@ describe('FeatureTable', () => { onChange={jest.fn()} onChangeAll={jest.fn()} spacesIndex={0} - intl={null as any} /> ); @@ -141,7 +139,6 @@ describe('FeatureTable', () => { onChange={jest.fn()} onChangeAll={jest.fn()} spacesIndex={-1} - intl={null as any} /> ); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.tsx similarity index 91% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.tsx index a05dc687fce4ac..8283efe23260af 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.tsx @@ -4,28 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ +import _ from 'lodash'; +import React, { Component } from 'react'; import { - // @ts-ignore EuiButtonGroup, EuiIcon, EuiIconTip, - // @ts-ignore EuiInMemoryTable, EuiText, IconType, } from '@elastic/eui'; -import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; -import _ from 'lodash'; -import React, { Component } from 'react'; -import { Feature } from '../../../../../../../../../../../plugins/features/public'; -import { FeaturesPrivileges, KibanaPrivileges, Role } from '../../../../../../../../common/model'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Feature } from '../../../../../../../../features/public'; +import { FeaturesPrivileges, KibanaPrivileges, Role } from '../../../../../../../common/model'; import { AllowedPrivilege, CalculatedPrivilege, PrivilegeExplanation, -} from '../../../../../../../lib/kibana_privilege_calculator'; -import { isGlobalPrivilegeDefinition } from '../../../../../../../lib/privilege_utils'; -import { NO_PRIVILEGE_VALUE } from '../../../../lib/constants'; +} from '../kibana_privilege_calculator'; +import { isGlobalPrivilegeDefinition } from '../../../privilege_utils'; +import { NO_PRIVILEGE_VALUE } from '../constants'; import { PrivilegeDisplay } from '../space_aware_privilege_section/privilege_display'; import { ChangeAllPrivilegesControl } from './change_all_privileges'; @@ -36,7 +35,6 @@ interface Props { allowedPrivileges: AllowedPrivilege; rankedFeaturePrivileges: FeaturesPrivileges; kibanaPrivileges: KibanaPrivileges; - intl: InjectedIntl; spacesIndex: number; onChange: (featureId: string, privileges: string[]) => void; onChangeAll: (privileges: string[]) => void; @@ -100,9 +98,7 @@ export class FeatureTable extends Component { const availablePrivileges = Object.values(rankedFeaturePrivileges)[0]; return ( - // @ts-ignore missing responsive from typedef { private getColumns = (availablePrivileges: string[]) => [ { field: 'feature', - name: this.props.intl.formatMessage({ - id: 'xpack.security.management.editRole.featureTable.enabledRoleFeaturesFeatureColumnTitle', - defaultMessage: 'Feature', - }), + name: i18n.translate( + 'xpack.security.management.editRole.featureTable.enabledRoleFeaturesFeatureColumnTitle', + { defaultMessage: 'Feature' } + ), render: (feature: TableFeature) => { let tooltipElement = null; if (feature.privilegesTooltip) { @@ -239,9 +235,7 @@ export class FeatureTable extends Component { }); return ( - // @ts-ignore missing name from typedef void; validator: RoleValidator; - intl: InjectedIntl; } export class KibanaPrivilegesRegion extends Component { @@ -81,7 +79,6 @@ export class KibanaPrivilegesRegion extends Component { privilegeCalculatorFactory={privilegeCalculatorFactory} onChange={onChange} editable={editable} - intl={this.props.intl} /> ); } diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/__snapshots__/simple_privilege_section.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/__snapshots__/simple_privilege_section.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/__snapshots__/simple_privilege_section.test.tsx.snap rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/__snapshots__/simple_privilege_section.test.tsx.snap diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/index.ts b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/index.ts rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/privilege_selector.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/privilege_selector.tsx similarity index 95% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/privilege_selector.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/privilege_selector.tsx index 135419cc9a10d2..bda0227372c094 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/privilege_selector.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/privilege_selector.tsx @@ -6,7 +6,7 @@ import { EuiSelect } from '@elastic/eui'; import React, { ChangeEvent, Component } from 'react'; -import { NO_PRIVILEGE_VALUE } from '../../../../lib/constants'; +import { NO_PRIVILEGE_VALUE } from '../constants'; interface Props { ['data-test-subj']: string; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx similarity index 95% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx index f97fa93294ff5b..db1e3cfd616212 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx @@ -3,13 +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. */ -// @ts-ignore + import { EuiButtonGroup, EuiButtonGroupProps, EuiComboBox, EuiSuperSelect } from '@elastic/eui'; import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { Feature } from '../../../../../../../../../../../plugins/features/public'; -import { KibanaPrivileges, Role } from '../../../../../../../../common/model'; -import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator'; +import { Feature } from '../../../../../../../../features/public'; +import { KibanaPrivileges, Role } from '../../../../../../../common/model'; +import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator'; import { SimplePrivilegeSection } from './simple_privilege_section'; import { UnsupportedSpacePrivilegesWarning } from './unsupported_space_privileges_warning'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.tsx similarity index 94% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.tsx index 7768dc769a32f7..2221fc6bab2797 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.tsx @@ -6,21 +6,24 @@ import { EuiComboBox, - // @ts-ignore EuiDescribedFormGroup, EuiFormRow, EuiSuperSelect, EuiText, } from '@elastic/eui'; -import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component, Fragment } from 'react'; -import { Feature } from '../../../../../../../../../../../plugins/features/public'; -import { KibanaPrivileges, Role, RoleKibanaPrivilege } from '../../../../../../../../common/model'; -import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator'; -import { isGlobalPrivilegeDefinition } from '../../../../../../../lib/privilege_utils'; -import { copyRole } from '../../../../../../../lib/role_utils'; -import { CUSTOM_PRIVILEGE_VALUE, NO_PRIVILEGE_VALUE } from '../../../../lib/constants'; +import { Feature } from '../../../../../../../../features/public'; +import { + KibanaPrivileges, + Role, + RoleKibanaPrivilege, + copyRole, +} from '../../../../../../../common/model'; +import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator'; +import { isGlobalPrivilegeDefinition } from '../../../privilege_utils'; +import { CUSTOM_PRIVILEGE_VALUE, NO_PRIVILEGE_VALUE } from '../constants'; import { FeatureTable } from '../feature_table'; import { UnsupportedSpacePrivilegesWarning } from './unsupported_space_privileges_warning'; @@ -31,7 +34,6 @@ interface Props { features: Feature[]; onChange: (role: Role) => void; editable: boolean; - intl: InjectedIntl; } interface State { @@ -230,7 +232,6 @@ export class SimplePrivilegeSection extends Component { allowedPrivileges={allowedPrivileges} rankedFeaturePrivileges={privilegeCalculator.rankedFeaturePrivileges} features={this.props.features} - intl={this.props.intl} onChange={this.onFeaturePrivilegeChange} onChangeAll={this.onChangeAllFeaturePrivileges} spacesIndex={this.props.role.kibana.findIndex(k => isGlobalPrivilegeDefinition(k))} diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/unsupported_space_privileges_warning.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/unsupported_space_privileges_warning.tsx similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/unsupported_space_privileges_warning.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/unsupported_space_privileges_warning.tsx diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__fixtures__/index.ts b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__fixtures__/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__fixtures__/index.ts rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__fixtures__/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__fixtures__/raw_kibana_privileges.ts b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__fixtures__/raw_kibana_privileges.ts similarity index 94% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__fixtures__/raw_kibana_privileges.ts rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__fixtures__/raw_kibana_privileges.ts index d412ba63403e19..428836c9f181b9 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__fixtures__/raw_kibana_privileges.ts +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__fixtures__/raw_kibana_privileges.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RawKibanaPrivileges } from '../../../../../../../../../common/model'; +import { RawKibanaPrivileges } from '../../../../../../../../common/model'; export const rawKibanaPrivileges: RawKibanaPrivileges = { global: { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_display.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_display.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_display.test.tsx.snap rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_display.test.tsx.snap diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap similarity index 82% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap index c20a391cdb20c5..e9f2f946e98859 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap @@ -351,108 +351,6 @@ exports[` renders without crashing 1`] = ` } disabled={true} features={Array []} - intl={ - Object { - "defaultFormats": Object {}, - "defaultLocale": "en", - "formatDate": [Function], - "formatHTMLMessage": [Function], - "formatMessage": [Function], - "formatNumber": [Function], - "formatPlural": [Function], - "formatRelative": [Function], - "formatTime": [Function], - "formats": Object { - "date": Object { - "full": Object { - "day": "numeric", - "month": "long", - "weekday": "long", - "year": "numeric", - }, - "long": Object { - "day": "numeric", - "month": "long", - "year": "numeric", - }, - "medium": Object { - "day": "numeric", - "month": "short", - "year": "numeric", - }, - "short": Object { - "day": "numeric", - "month": "numeric", - "year": "2-digit", - }, - }, - "number": Object { - "currency": Object { - "style": "currency", - }, - "percent": Object { - "style": "percent", - }, - }, - "relative": Object { - "days": Object { - "units": "day", - }, - "hours": Object { - "units": "hour", - }, - "minutes": Object { - "units": "minute", - }, - "months": Object { - "units": "month", - }, - "seconds": Object { - "units": "second", - }, - "years": Object { - "units": "year", - }, - }, - "time": Object { - "full": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short", - }, - "long": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short", - }, - "medium": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - }, - "short": Object { - "hour": "numeric", - "minute": "numeric", - }, - }, - }, - "formatters": Object { - "getDateTimeFormat": [Function], - "getMessageFormat": [Function], - "getNumberFormat": [Function], - "getPluralFormat": [Function], - "getRelativeFormat": [Function], - }, - "locale": "en", - "messages": Object {}, - "now": [Function], - "onError": [Function], - "textComponent": Symbol(react.fragment), - "timeZone": null, - } - } kibanaPrivileges={ KibanaPrivileges { "rawKibanaPrivileges": Object { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/space_aware_privilege_section.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/space_aware_privilege_section.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/space_aware_privilege_section.test.tsx.snap rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/space_aware_privilege_section.test.tsx.snap diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/_index.scss similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/_index.scss rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/_index.scss diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/_privilege_matrix.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/_privilege_matrix.scss similarity index 87% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/_privilege_matrix.scss rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/_privilege_matrix.scss index 5f9fbced5ee6ac..8f47727fdf8d62 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/_privilege_matrix.scss +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/_privilege_matrix.scss @@ -9,6 +9,6 @@ .secPrivilegeMatrix__row--isBasePrivilege, .secPrivilegeMatrix__cell--isGlobalPrivilege, -.secPrivilegeTable__row--isGlobalSpace, { +.secPrivilegeTable__row--isGlobalSpace { background-color: $euiColorLightestShade; } diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/index.ts b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/index.ts rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_display.test.tsx similarity index 96% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_display.test.tsx index 62e22050132fd7..c6268e19abfd1f 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_display.test.tsx @@ -7,7 +7,7 @@ import { EuiIconTip, EuiText, EuiToolTip } from '@elastic/eui'; import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { PRIVILEGE_SOURCE } from '../../../../../../../lib/kibana_privilege_calculator'; +import { PRIVILEGE_SOURCE } from '../kibana_privilege_calculator'; import { PrivilegeDisplay } from './privilege_display'; describe('PrivilegeDisplay', () => { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_display.tsx similarity index 96% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_display.tsx index 6af7672f6fef86..55ac99da4c8c16 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_display.tsx @@ -7,11 +7,8 @@ import { EuiIcon, EuiIconTip, EuiText, IconType, PropsOf, EuiToolTip } from '@el import { FormattedMessage } from '@kbn/i18n/react'; import _ from 'lodash'; import React, { ReactNode, FC } from 'react'; -import { - PRIVILEGE_SOURCE, - PrivilegeExplanation, -} from '../../../../../../../lib/kibana_privilege_calculator'; -import { NO_PRIVILEGE_VALUE } from '../../../../lib/constants'; +import { PRIVILEGE_SOURCE, PrivilegeExplanation } from '../kibana_privilege_calculator'; +import { NO_PRIVILEGE_VALUE } from '../constants'; interface Props extends PropsOf { privilege: string | string[] | undefined; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_matrix.test.tsx similarity index 89% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_matrix.test.tsx index ee121caa13a2af..16aad4826ae44c 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_matrix.test.tsx @@ -3,14 +3,14 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore + import { EuiButtonEmpty, EuiInMemoryTable } from '@elastic/eui'; import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { Space } from '../../../../../../../../../spaces/common/model/space'; -import { Feature } from '../../../../../../../../../../../plugins/features/public'; -import { KibanaPrivileges, Role } from '../../../../../../../../common/model'; -import { KibanaPrivilegeCalculatorFactory } from '../../../../../../..//lib/kibana_privilege_calculator'; +import { Space } from '../../../../../../../../spaces/common/model/space'; +import { Feature } from '../../../../../../../../features/public'; +import { KibanaPrivileges, Role } from '../../../../../../../common/model'; +import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator'; import { PrivilegeMatrix } from './privilege_matrix'; describe('PrivilegeMatrix', () => { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_matrix.tsx similarity index 93% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_matrix.tsx index 962487312c83df..b3449e32c6c917 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_matrix.tsx @@ -8,7 +8,6 @@ import { EuiButtonEmpty, EuiIcon, EuiIconTip, - // @ts-ignore EuiInMemoryTable, EuiModal, EuiModalBody, @@ -16,18 +15,16 @@ import { EuiModalHeader, EuiModalHeaderTitle, EuiOverlayMask, - // @ts-ignore - EuiToolTip, IconType, } from '@elastic/eui'; import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; import React, { Component, Fragment } from 'react'; -import { Space } from '../../../../../../../../../spaces/common/model/space'; -import { SpaceAvatar } from '../../../../../../../../../spaces/public/space_avatar'; -import { Feature } from '../../../../../../../../../../../plugins/features/public'; -import { FeaturesPrivileges, Role } from '../../../../../../../../common/model'; -import { CalculatedPrivilege } from '../../../../../../../lib/kibana_privilege_calculator'; -import { isGlobalPrivilegeDefinition } from '../../../../../../../lib/privilege_utils'; +import { SpaceAvatar } from '../../../../../../../../../legacy/plugins/spaces/public/space_avatar'; +import { Space } from '../../../../../../../../spaces/common/model/space'; +import { Feature } from '../../../../../../../../features/public'; +import { FeaturesPrivileges, Role } from '../../../../../../../common/model'; +import { CalculatedPrivilege } from '../kibana_privilege_calculator'; +import { isGlobalPrivilegeDefinition } from '../../../privilege_utils'; import { SpacesPopoverList } from '../../../spaces_popover_list'; import { PrivilegeDisplay } from './privilege_display'; @@ -258,11 +255,9 @@ export class PrivilegeMatrix extends Component { ]; return ( - // @ts-ignore missing rowProps from typedef { return { className: item.feature.isBase ? 'secPrivilegeMatrix__row--isBasePrivilege' : '', diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.test.tsx similarity index 95% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.test.tsx index 2b7d87f663d729..675f02a81f9e1d 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.test.tsx @@ -9,8 +9,8 @@ import { merge } from 'lodash'; // @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; import { shallowWithIntl, mountWithIntl } from 'test_utils/enzyme_helpers'; -import { KibanaPrivileges } from '../../../../../../../../common/model'; -import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator'; +import { KibanaPrivileges } from '../../../../../../../common/model'; +import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator'; import { PrivilegeSpaceForm } from './privilege_space_form'; import { rawKibanaPrivileges } from './__fixtures__'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx similarity index 96% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx index 5abb87d23bb6e6..6d1f5117c52e98 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx @@ -24,17 +24,16 @@ import { } from '@elastic/eui'; import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; import React, { Component, Fragment } from 'react'; -import { Space } from '../../../../../../../../../spaces/common/model/space'; -import { Feature } from '../../../../../../../../../../../plugins/features/public'; -import { KibanaPrivileges, Role } from '../../../../../../../../common/model'; +import { Space } from '../../../../../../../../spaces/common/model/space'; +import { Feature } from '../../../../../../../../features/public'; +import { KibanaPrivileges, Role, copyRole } from '../../../../../../../common/model'; import { AllowedPrivilege, KibanaPrivilegeCalculatorFactory, PrivilegeExplanation, -} from '../../../../../../../lib/kibana_privilege_calculator'; -import { hasAssignedFeaturePrivileges } from '../../../../../../../lib/privilege_utils'; -import { copyRole } from '../../../../../../../lib/role_utils'; -import { CUSTOM_PRIVILEGE_VALUE } from '../../../../lib/constants'; +} from '../kibana_privilege_calculator'; +import { hasAssignedFeaturePrivileges } from '../../../privilege_utils'; +import { CUSTOM_PRIVILEGE_VALUE } from '../constants'; import { FeatureTable } from '../feature_table'; import { SpaceSelector } from './space_selector'; @@ -285,7 +284,6 @@ export class PrivilegeSpaceForm extends Component { calculatedPrivileges={calculatedPrivileges} allowedPrivileges={allowedPrivileges} rankedFeaturePrivileges={privilegeCalculator.rankedFeaturePrivileges} - intl={this.props.intl} onChange={this.onFeaturePrivilegesChange} onChangeAll={this.onChangeAllFeaturePrivileges} kibanaPrivileges={this.props.kibanaPrivileges} diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx similarity index 99% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx index 37ee43c5473b0d..f0a391c98c9100 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx @@ -10,8 +10,8 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ReactWrapper } from 'enzyme'; import { PrivilegeSpaceTable } from './privilege_space_table'; import { PrivilegeDisplay } from './privilege_display'; -import { KibanaPrivileges, Role, RoleKibanaPrivilege } from '../../../../../../../../common/model'; -import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator'; +import { KibanaPrivileges, Role, RoleKibanaPrivilege } from '../../../../../../../common/model'; +import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator'; import { rawKibanaPrivileges } from './__fixtures__'; interface TableRow { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx similarity index 94% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx index 65a3df9fb47a1a..1c27ec84f50dcd 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx @@ -12,22 +12,21 @@ import { EuiBasicTableColumn, } from '@elastic/eui'; import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; -import _ from 'lodash'; import React, { Component } from 'react'; -import { getSpaceColor } from '../../../../../../../../../spaces/public/space_avatar'; -import { Space } from '../../../../../../../../../spaces/common/model/space'; +import { getSpaceColor } from '../../../../../../../../../legacy/plugins/spaces/public/space_avatar'; +import { Space } from '../../../../../../../../spaces/common/model/space'; import { FeaturesPrivileges, Role, RoleKibanaPrivilege, -} from '../../../../../../../../common/model'; -import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator'; + copyRole, +} from '../../../../../../../common/model'; +import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator'; import { isGlobalPrivilegeDefinition, hasAssignedFeaturePrivileges, -} from '../../../../../../../lib/privilege_utils'; -import { copyRole } from '../../../../../../../lib/role_utils'; -import { CUSTOM_PRIVILEGE_VALUE, NO_PRIVILEGE_VALUE } from '../../../../lib/constants'; +} from '../../../privilege_utils'; +import { CUSTOM_PRIVILEGE_VALUE, NO_PRIVILEGE_VALUE } from '../constants'; import { SpacesPopoverList } from '../../../spaces_popover_list'; import { PrivilegeDisplay } from './privilege_display'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.test.tsx similarity index 95% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.test.tsx index 2756b1c4472744..e06d2a4f7dc337 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.test.tsx @@ -6,9 +6,9 @@ import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { KibanaPrivileges } from '../../../../../../../../common/model'; -import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator'; -import { RoleValidator } from '../../../../lib/validate_role'; +import { KibanaPrivileges } from '../../../../../../../common/model'; +import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator'; +import { RoleValidator } from '../../../validate_role'; import { PrivilegeMatrix } from './privilege_matrix'; import { PrivilegeSpaceForm } from './privilege_space_form'; import { PrivilegeSpaceTable } from './privilege_space_table'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx similarity index 93% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx index d324cf99c8418a..21cadfafe1790b 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx @@ -14,13 +14,12 @@ import { import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import _ from 'lodash'; import React, { Component, Fragment } from 'react'; -import { UICapabilities } from 'ui/capabilities'; -import { Space } from '../../../../../../../../../spaces/common/model/space'; -import { Feature } from '../../../../../../../../../../../plugins/features/public'; -import { KibanaPrivileges, Role } from '../../../../../../../../common/model'; -import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator'; -import { isReservedRole } from '../../../../../../../lib/role_utils'; -import { RoleValidator } from '../../../../lib/validate_role'; +import { Capabilities } from 'src/core/public'; +import { Space } from '../../../../../../../../spaces/common/model/space'; +import { Feature } from '../../../../../../../../features/public'; +import { KibanaPrivileges, Role, isReservedRole } from '../../../../../../../common/model'; +import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator'; +import { RoleValidator } from '../../../validate_role'; import { PrivilegeMatrix } from './privilege_matrix'; import { PrivilegeSpaceForm } from './privilege_space_form'; import { PrivilegeSpaceTable } from './privilege_space_table'; @@ -34,7 +33,7 @@ interface Props { editable: boolean; validator: RoleValidator; intl: InjectedIntl; - uiCapabilities: UICapabilities; + uiCapabilities: Capabilities; features: Feature[]; } diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_selector.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_selector.tsx similarity index 93% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_selector.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_selector.tsx index 0eb9cf0b0ee9d7..cfeb5b9f37d8ce 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_selector.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_selector.tsx @@ -7,8 +7,8 @@ import { EuiComboBox, EuiComboBoxOptionProps, EuiHealth, EuiHighlight } from '@elastic/eui'; import { InjectedIntl } from '@kbn/i18n/react'; import React, { Component } from 'react'; -import { Space } from '../../../../../../../../../spaces/common/model/space'; -import { getSpaceColor } from '../../../../../../../../../spaces/public/space_avatar'; +import { getSpaceColor } from '../../../../../../../../../legacy/plugins/spaces/public/space_avatar'; +import { Space } from '../../../../../../../../spaces/common/model/space'; const spaceToOption = (space?: Space, currentSelection?: 'global' | 'spaces') => { if (!space) { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/transform_error_section/index.ts b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/transform_error_section/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/transform_error_section/index.ts rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/transform_error_section/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/transform_error_section/transform_error_section.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/transform_error_section/transform_error_section.tsx similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/transform_error_section/transform_error_section.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/transform_error_section/transform_error_section.tsx diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/reserved_role_badge.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/reserved_role_badge.test.tsx similarity index 96% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/reserved_role_badge.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/reserved_role_badge.test.tsx index 9b483d92cde41b..d29b442420a90c 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/reserved_role_badge.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/reserved_role_badge.test.tsx @@ -7,7 +7,7 @@ import { EuiIcon } from '@elastic/eui'; import { shallow } from 'enzyme'; import React from 'react'; -import { Role } from '../../../../../common/model'; +import { Role } from '../../../../common/model'; import { ReservedRoleBadge } from './reserved_role_badge'; const reservedRole: Role = { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/reserved_role_badge.tsx b/x-pack/plugins/security/public/management/roles/edit_role/reserved_role_badge.tsx similarity index 89% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/reserved_role_badge.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/reserved_role_badge.tsx index 3d817d1e07d212..501ca7589dafde 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/reserved_role_badge.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/reserved_role_badge.tsx @@ -8,8 +8,7 @@ import React from 'react'; import { EuiIcon, EuiToolTip } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Role } from '../../../../../common/model'; -import { isReservedRole } from '../../../../lib/role_utils'; +import { Role, isReservedRole } from '../../../../common/model'; interface Props { role: Role; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/_index.scss new file mode 100644 index 00000000000000..b40a32cb8df96b --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/_index.scss @@ -0,0 +1 @@ +@import './spaces_popover_list'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/spaces_popover_list/_spaces_popover_list.scss b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/_spaces_popover_list.scss similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/spaces_popover_list/_spaces_popover_list.scss rename to x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/_spaces_popover_list.scss diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/spaces_popover_list/index.ts b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/spaces_popover_list/index.ts rename to x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/spaces_popover_list/spaces_popover_list.tsx b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.tsx similarity index 95% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/spaces_popover_list/spaces_popover_list.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.tsx index a99e389044eaad..bb7a6db97f7c88 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/spaces_popover_list/spaces_popover_list.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.tsx @@ -14,9 +14,9 @@ import { } from '@elastic/eui'; import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; import React, { Component } from 'react'; -import { SpaceAvatar } from '../../../../../../../spaces/public/space_avatar'; -import { SPACE_SEARCH_COUNT_THRESHOLD } from '../../../../../../../../../plugins/spaces/common/constants'; -import { Space } from '../../../../../../../../../plugins/spaces/common/model/space'; +import { SpaceAvatar } from '../../../../../../../legacy/plugins/spaces/public/space_avatar'; +import { SPACE_SEARCH_COUNT_THRESHOLD } from '../../../../../../spaces/common'; +import { Space } from '../../../../../../spaces/common/model/space'; interface Props { spaces: Space[]; @@ -146,7 +146,6 @@ export class SpacesPopoverList extends Component { return (
{ - // @ts-ignore onSearch isn't defined on the type ({ + getFields: jest.fn(), + }), +}; diff --git a/x-pack/plugins/security/public/management/roles/indices_api_client.ts b/x-pack/plugins/security/public/management/roles/indices_api_client.ts new file mode 100644 index 00000000000000..65d9a40a776eb0 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/indices_api_client.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 { HttpStart } from 'src/core/public'; + +export class IndicesAPIClient { + constructor(private readonly http: HttpStart) {} + + async getFields(query: string) { + return ( + (await this.http.get(`/internal/security/fields/${encodeURIComponent(query)}`)) || + [] + ); + } +} diff --git a/x-pack/plugins/security/public/management/roles/privileges_api_client.mock.ts b/x-pack/plugins/security/public/management/roles/privileges_api_client.mock.ts new file mode 100644 index 00000000000000..2564914a1d3d83 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/privileges_api_client.mock.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 const privilegesAPIClientMock = { + create: () => ({ + getAll: jest.fn(), + getBuiltIn: jest.fn(), + }), +}; diff --git a/x-pack/plugins/security/public/management/roles/privileges_api_client.ts b/x-pack/plugins/security/public/management/roles/privileges_api_client.ts new file mode 100644 index 00000000000000..45bd2fd8fb3a6a --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/privileges_api_client.ts @@ -0,0 +1,22 @@ +/* + * 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 { HttpStart } from 'src/core/public'; +import { BuiltinESPrivileges, RawKibanaPrivileges } from '../../../common/model'; + +export class PrivilegesAPIClient { + constructor(private readonly http: HttpStart) {} + + async getAll({ includeActions }: { includeActions: boolean }) { + return await this.http.get('/api/security/privileges', { + query: { includeActions }, + }); + } + + async getBuiltIn() { + return await this.http.get('/internal/security/esPrivileges/builtin'); + } +} diff --git a/x-pack/plugins/security/public/management/roles/roles_api_client.mock.ts b/x-pack/plugins/security/public/management/roles/roles_api_client.mock.ts new file mode 100644 index 00000000000000..c4d3724c0ecb5f --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/roles_api_client.mock.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. + */ + +export const rolesAPIClientMock = { + create: () => ({ + getRoles: jest.fn(), + getRole: jest.fn(), + deleteRole: jest.fn(), + saveRole: jest.fn(), + }), +}; diff --git a/x-pack/legacy/plugins/security/public/lib/transform_role_for_save.test.ts b/x-pack/plugins/security/public/management/roles/roles_api_client.test.ts similarity index 82% rename from x-pack/legacy/plugins/security/public/lib/transform_role_for_save.test.ts rename to x-pack/plugins/security/public/management/roles/roles_api_client.test.ts index 1ea19f2637305e..75611613684051 100644 --- a/x-pack/legacy/plugins/security/public/lib/transform_role_for_save.test.ts +++ b/x-pack/plugins/security/public/management/roles/roles_api_client.test.ts @@ -4,12 +4,23 @@ * 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'; +import { Role } from '../../../common/model'; +import { RolesAPIClient } from './roles_api_client'; +import { httpServiceMock } from '../../../../../../src/core/public/mocks'; + +describe('RolesAPIClient', () => { + async function saveRole(role: Role, spacesEnabled: boolean) { + const httpMock = httpServiceMock.createStartContract(); + const rolesAPIClient = new RolesAPIClient(httpMock); + + await rolesAPIClient.saveRole({ role, spacesEnabled }); + expect(httpMock.put).toHaveBeenCalledTimes(1); + + return JSON.parse(httpMock.put.mock.calls[0][1]?.body as any); + } -describe('transformRoleForSave', () => { describe('spaces disabled', () => { - it('removes placeholder index privileges', () => { + it('removes placeholder index privileges', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -20,7 +31,7 @@ describe('transformRoleForSave', () => { kibana: [], }; - const result = transformRoleForSave(role, false); + const result = await saveRole(role, false); expect(result).toEqual({ elasticsearch: { @@ -32,7 +43,7 @@ describe('transformRoleForSave', () => { }); }); - it('removes placeholder query entries', () => { + it('removes placeholder query entries', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -43,7 +54,7 @@ describe('transformRoleForSave', () => { kibana: [], }; - const result = transformRoleForSave(role, false); + const result = await saveRole(role, false); expect(result).toEqual({ elasticsearch: { @@ -55,7 +66,7 @@ describe('transformRoleForSave', () => { }); }); - it('removes transient fields not required for save', () => { + it('removes transient fields not required for save', async () => { const role: Role = { name: 'my role', transient_metadata: { @@ -74,7 +85,7 @@ describe('transformRoleForSave', () => { kibana: [], }; - const result = transformRoleForSave(role, false); + const result = await saveRole(role, false); expect(result).toEqual({ metadata: { @@ -89,7 +100,7 @@ describe('transformRoleForSave', () => { }); }); - it('does not remove actual query entries', () => { + it('does not remove actual query entries', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -100,7 +111,7 @@ describe('transformRoleForSave', () => { kibana: [], }; - const result = transformRoleForSave(role, false); + const result = await saveRole(role, false); expect(result).toEqual({ elasticsearch: { @@ -112,7 +123,7 @@ describe('transformRoleForSave', () => { }); }); - it('should remove feature privileges if a corresponding base privilege is defined', () => { + it('should remove feature privileges if a corresponding base privilege is defined', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -132,7 +143,7 @@ describe('transformRoleForSave', () => { ], }; - const result = transformRoleForSave(role, false); + const result = await saveRole(role, false); expect(result).toEqual({ elasticsearch: { @@ -150,7 +161,7 @@ describe('transformRoleForSave', () => { }); }); - it('should not remove feature privileges if a corresponding base privilege is not defined', () => { + it('should not remove feature privileges if a corresponding base privilege is not defined', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -170,7 +181,7 @@ describe('transformRoleForSave', () => { ], }; - const result = transformRoleForSave(role, false); + const result = await saveRole(role, false); expect(result).toEqual({ elasticsearch: { @@ -191,7 +202,7 @@ describe('transformRoleForSave', () => { }); }); - it('should remove space privileges', () => { + it('should remove space privileges', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -219,7 +230,7 @@ describe('transformRoleForSave', () => { ], }; - const result = transformRoleForSave(role, false); + const result = await saveRole(role, false); expect(result).toEqual({ elasticsearch: { @@ -242,7 +253,7 @@ describe('transformRoleForSave', () => { }); describe('spaces enabled', () => { - it('removes placeholder index privileges', () => { + it('removes placeholder index privileges', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -253,7 +264,7 @@ describe('transformRoleForSave', () => { kibana: [], }; - const result = transformRoleForSave(role, true); + const result = await saveRole(role, true); expect(result).toEqual({ elasticsearch: { @@ -265,7 +276,7 @@ describe('transformRoleForSave', () => { }); }); - it('removes placeholder query entries', () => { + it('removes placeholder query entries', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -276,7 +287,7 @@ describe('transformRoleForSave', () => { kibana: [], }; - const result = transformRoleForSave(role, true); + const result = await saveRole(role, true); expect(result).toEqual({ elasticsearch: { @@ -288,7 +299,7 @@ describe('transformRoleForSave', () => { }); }); - it('removes transient fields not required for save', () => { + it('removes transient fields not required for save', async () => { const role: Role = { name: 'my role', transient_metadata: { @@ -307,7 +318,7 @@ describe('transformRoleForSave', () => { kibana: [], }; - const result = transformRoleForSave(role, true); + const result = await saveRole(role, true); expect(result).toEqual({ metadata: { @@ -322,7 +333,7 @@ describe('transformRoleForSave', () => { }); }); - it('does not remove actual query entries', () => { + it('does not remove actual query entries', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -333,7 +344,7 @@ describe('transformRoleForSave', () => { kibana: [], }; - const result = transformRoleForSave(role, true); + const result = await saveRole(role, true); expect(result).toEqual({ elasticsearch: { @@ -345,7 +356,7 @@ describe('transformRoleForSave', () => { }); }); - it('should remove feature privileges if a corresponding base privilege is defined', () => { + it('should remove feature privileges if a corresponding base privilege is defined', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -365,7 +376,7 @@ describe('transformRoleForSave', () => { ], }; - const result = transformRoleForSave(role, true); + const result = await saveRole(role, true); expect(result).toEqual({ elasticsearch: { @@ -383,7 +394,7 @@ describe('transformRoleForSave', () => { }); }); - it('should not remove feature privileges if a corresponding base privilege is not defined', () => { + it('should not remove feature privileges if a corresponding base privilege is not defined', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -403,7 +414,7 @@ describe('transformRoleForSave', () => { ], }; - const result = transformRoleForSave(role, true); + const result = await saveRole(role, true); expect(result).toEqual({ elasticsearch: { @@ -424,7 +435,7 @@ describe('transformRoleForSave', () => { }); }); - it('should not remove space privileges', () => { + it('should not remove space privileges', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -452,7 +463,7 @@ describe('transformRoleForSave', () => { ], }; - const result = transformRoleForSave(role, true); + const result = await saveRole(role, true); expect(result).toEqual({ elasticsearch: { diff --git a/x-pack/plugins/security/public/management/roles/roles_api_client.ts b/x-pack/plugins/security/public/management/roles/roles_api_client.ts new file mode 100644 index 00000000000000..d7e98e03a965b7 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/roles_api_client.ts @@ -0,0 +1,62 @@ +/* + * 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 { HttpStart } from 'src/core/public'; +import { Role, RoleIndexPrivilege, copyRole } from '../../../common/model'; +import { isGlobalPrivilegeDefinition } from './edit_role/privilege_utils'; + +export class RolesAPIClient { + constructor(private readonly http: HttpStart) {} + + public async getRoles() { + return await this.http.get('/api/security/role'); + } + + public async getRole(roleName: string) { + return await this.http.get(`/api/security/role/${encodeURIComponent(roleName)}`); + } + + public async deleteRole(roleName: string) { + await this.http.delete(`/api/security/role/${encodeURIComponent(roleName)}`); + } + + public async saveRole({ role, spacesEnabled }: { role: Role; spacesEnabled: boolean }) { + await this.http.put(`/api/security/role/${encodeURIComponent(role.name)}`, { + body: JSON.stringify(this.transformRoleForSave(copyRole(role), spacesEnabled)), + }); + } + + private transformRoleForSave(role: Role, spacesEnabled: boolean) { + // Remove any placeholder index privileges + const isPlaceholderPrivilege = (indexPrivilege: RoleIndexPrivilege) => + indexPrivilege.names.length === 0; + 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; + } +} diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/__snapshots__/roles_grid_page.test.tsx.snap b/x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/roles_grid/components/__snapshots__/roles_grid_page.test.tsx.snap rename to x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/confirm_delete/confirm_delete.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/confirm_delete/confirm_delete.tsx similarity index 72% rename from x-pack/legacy/plugins/security/public/views/management/roles_grid/components/confirm_delete/confirm_delete.tsx rename to x-pack/plugins/security/public/management/roles/roles_grid/confirm_delete/confirm_delete.tsx index 34784b4b2accb6..37eed3357241d5 100644 --- a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/confirm_delete/confirm_delete.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/confirm_delete/confirm_delete.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import React, { Component, Fragment } from 'react'; import { EuiButton, EuiButtonEmpty, @@ -15,23 +16,24 @@ import { EuiOverlayMask, EuiText, } from '@elastic/eui'; -import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import React, { Component, Fragment } from 'react'; -import { toastNotifications } from 'ui/notify'; -import { RolesApi } from '../../../../../lib/roles_api'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { NotificationsStart } from 'src/core/public'; +import { RolesAPIClient } from '../../roles_api_client'; interface Props { rolesToDelete: string[]; - intl: InjectedIntl; callback: (rolesToDelete: string[], errors: string[]) => void; onCancel: () => void; + notifications: NotificationsStart; + rolesAPIClient: PublicMethodsOf; } interface State { deleteInProgress: boolean; } -class ConfirmDeleteUI extends Component { +export class ConfirmDelete extends Component { constructor(props: Props) { super(props); this.state = { @@ -40,15 +42,12 @@ class ConfirmDeleteUI extends Component { } public render() { - const { rolesToDelete, intl } = this.props; + const { rolesToDelete } = this.props; const moreThanOne = rolesToDelete.length > 1; - const title = intl.formatMessage( - { - id: 'xpack.security.management.roles.deleteRoleTitle', - defaultMessage: 'Delete role{value, plural, one {{roleName}} other {s}}', - }, - { value: rolesToDelete.length, roleName: ` ${rolesToDelete[0]}` } - ); + const title = i18n.translate('xpack.security.management.roles.deleteRoleTitle', { + defaultMessage: 'Delete role{value, plural, one {{roleName}} other {s}}', + values: { value: rolesToDelete.length, roleName: ` ${rolesToDelete[0]}` }, + }); // This is largely the same as the built-in EuiConfirmModal component, but we needed the ability // to disable the buttons since this could be a long-running operation @@ -128,32 +127,24 @@ class ConfirmDeleteUI extends Component { }; private deleteRoles = async () => { - const { rolesToDelete, callback } = this.props; + const { rolesToDelete, callback, rolesAPIClient, notifications } = this.props; const errors: string[] = []; const deleteOperations = rolesToDelete.map(roleName => { const deleteRoleTask = async () => { try { - await RolesApi.deleteRole(roleName); - toastNotifications.addSuccess( - this.props.intl.formatMessage( - { - id: - 'xpack.security.management.roles.confirmDelete.roleSuccessfullyDeletedNotificationMessage', - defaultMessage: 'Deleted role {roleName}', - }, - { roleName } + await rolesAPIClient.deleteRole(roleName); + notifications.toasts.addSuccess( + i18n.translate( + 'xpack.security.management.roles.confirmDelete.roleSuccessfullyDeletedNotificationMessage', + { defaultMessage: 'Deleted role {roleName}', values: { roleName } } ) ); } catch (e) { errors.push(roleName); - toastNotifications.addDanger( - this.props.intl.formatMessage( - { - id: - 'xpack.security.management.roles.confirmDelete.roleDeletingErrorNotificationMessage', - defaultMessage: 'Error deleting role {roleName}', - }, - { roleName } + notifications.toasts.addDanger( + i18n.translate( + 'xpack.security.management.roles.confirmDelete.roleDeletingErrorNotificationMessage', + { defaultMessage: 'Error deleting role {roleName}', values: { roleName } } ) ); } @@ -167,5 +158,3 @@ class ConfirmDeleteUI extends Component { callback(rolesToDelete, errors); }; } - -export const ConfirmDelete = injectI18n(ConfirmDeleteUI); diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/confirm_delete/index.ts b/x-pack/plugins/security/public/management/roles/roles_grid/confirm_delete/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/roles_grid/components/confirm_delete/index.ts rename to x-pack/plugins/security/public/management/roles/roles_grid/confirm_delete/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/index.ts b/x-pack/plugins/security/public/management/roles/roles_grid/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/roles_grid/components/index.ts rename to x-pack/plugins/security/public/management/roles/roles_grid/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/permission_denied/index.ts b/x-pack/plugins/security/public/management/roles/roles_grid/permission_denied/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/roles_grid/components/permission_denied/index.ts rename to x-pack/plugins/security/public/management/roles/roles_grid/permission_denied/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/permission_denied/permission_denied.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/permission_denied/permission_denied.tsx similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/roles_grid/components/permission_denied/permission_denied.tsx rename to x-pack/plugins/security/public/management/roles/roles_grid/permission_denied/permission_denied.tsx diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/roles_grid_page.test.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx similarity index 63% rename from x-pack/legacy/plugins/security/public/views/management/roles_grid/components/roles_grid_page.test.tsx rename to x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx index 6da2f2442d488b..63ace53420612d 100644 --- a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/roles_grid_page.test.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx @@ -4,50 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -let mockSimulate403 = false; -const mock403 = () => ({ body: { statusCode: 403 } }); -jest.mock('../../../../lib/roles_api', () => { - return { - RolesApi: { - async getRoles() { - if (mockSimulate403) { - throw mock403(); - } - return [ - { - name: 'test-role-1', - elasticsearch: { cluster: [], indices: [], run_as: [] }, - kibana: { global: [], space: {} }, - }, - { - name: 'reserved-role', - elasticsearch: { cluster: [], indices: [], run_as: [] }, - kibana: { global: [], space: {} }, - metadata: { - _reserved: true, - }, - }, - { - name: 'disabled-role', - elasticsearch: { cluster: [], indices: [], run_as: [] }, - kibana: { global: [], space: {} }, - transient_metadata: { - enabled: false, - }, - }, - ]; - }, - }, - }; -}); - import { EuiIcon } from '@elastic/eui'; import { ReactWrapper } from 'enzyme'; import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { RolesAPIClient } from '../roles_api_client'; import { PermissionDenied } from './permission_denied'; import { RolesGridPage } from './roles_grid_page'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { rolesAPIClientMock } from '../index.mock'; + +const mock403 = () => ({ body: { statusCode: 403 } }); + const waitForRender = async ( wrapper: ReactWrapper, condition: (wrapper: ReactWrapper) => boolean @@ -69,12 +38,37 @@ const waitForRender = async ( }; describe('', () => { + let apiClientMock: jest.Mocked>; beforeEach(() => { - mockSimulate403 = false; + apiClientMock = rolesAPIClientMock.create(); + apiClientMock.getRoles.mockResolvedValue([ + { + name: 'test-role-1', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [{ base: [], spaces: [], feature: {} }], + }, + { + name: 'reserved-role', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [{ base: [], spaces: [], feature: {} }], + metadata: { _reserved: true }, + }, + { + name: 'disabled-role', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [{ base: [], spaces: [], feature: {} }], + transient_metadata: { enabled: false }, + }, + ]); }); it(`renders reserved roles as such`, async () => { - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl( + + ); const initialIconCount = wrapper.find(EuiIcon).length; await waitForRender(wrapper, updatedWrapper => { @@ -87,8 +81,14 @@ describe('', () => { }); it('renders permission denied if required', async () => { - mockSimulate403 = true; - const wrapper = mountWithIntl(); + apiClientMock.getRoles.mockRejectedValue(mock403()); + + const wrapper = mountWithIntl( + + ); await waitForRender(wrapper, updatedWrapper => { return updatedWrapper.find(PermissionDenied).length > 0; }); @@ -96,7 +96,12 @@ describe('', () => { }); it('renders role actions as appropriate', async () => { - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl( + + ); const initialIconCount = wrapper.find(EuiIcon).length; await waitForRender(wrapper, updatedWrapper => { diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/roles_grid_page.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx similarity index 78% rename from x-pack/legacy/plugins/security/public/views/management/roles_grid/components/roles_grid_page.tsx rename to x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx index 2083a93f4b33c9..7c686bef391a79 100644 --- a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/roles_grid_page.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import _ from 'lodash'; +import React, { Component } from 'react'; import { EuiButton, EuiIcon, @@ -18,18 +20,17 @@ import { EuiButtonIcon, EuiBasicTableColumn, } from '@elastic/eui'; -import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import _ from 'lodash'; -import React, { Component } from 'react'; -import { toastNotifications } from 'ui/notify'; -import { Role } from '../../../../../common/model'; -import { isRoleEnabled, isReadOnlyRole, isReservedRole } from '../../../../lib/role_utils'; -import { RolesApi } from '../../../../lib/roles_api'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { NotificationsStart } from 'src/core/public'; +import { Role, isRoleEnabled, isReadOnlyRole, isReservedRole } from '../../../../common/model'; +import { RolesAPIClient } from '../roles_api_client'; import { ConfirmDelete } from './confirm_delete'; import { PermissionDenied } from './permission_denied'; interface Props { - intl: InjectedIntl; + notifications: NotificationsStart; + rolesAPIClient: PublicMethodsOf; } interface State { @@ -44,7 +45,7 @@ const getRoleManagementHref = (action: 'edit' | 'clone', roleName?: string) => { return `#/management/security/roles/${action}${roleName ? `/${roleName}` : ''}`; }; -class RolesGridPageUI extends Component { +export class RolesGridPage extends Component { constructor(props: Props) { super(props); this.state = { @@ -68,7 +69,6 @@ class RolesGridPageUI extends Component { private getPageContent = () => { const { roles } = this.state; - const { intl } = this.props; return ( @@ -105,6 +105,8 @@ class RolesGridPageUI extends Component { onCancel={this.onCancelDelete} rolesToDelete={this.state.selection.map(role => role.name)} callback={this.handleDelete} + notifications={this.props.notifications} + rolesAPIClient={this.props.rolesAPIClient} /> ) : null} @@ -112,7 +114,7 @@ class RolesGridPageUI extends Component { !role.metadata || !role.metadata._reserved, @@ -155,17 +157,16 @@ class RolesGridPageUI extends Component { ); }; - private getColumnConfig = (intl: InjectedIntl) => { - const reservedRoleDesc = intl.formatMessage({ - id: 'xpack.security.management.roles.reservedColumnDescription', - defaultMessage: 'Reserved roles are built-in and cannot be edited or removed.', - }); + private getColumnConfig = () => { + const reservedRoleDesc = i18n.translate( + 'xpack.security.management.roles.reservedColumnDescription', + { defaultMessage: 'Reserved roles are built-in and cannot be edited or removed.' } + ); return [ { field: 'name', - name: intl.formatMessage({ - id: 'xpack.security.management.roles.nameColumnName', + name: i18n.translate('xpack.security.management.roles.nameColumnName', { defaultMessage: 'Role', }), sortable: true, @@ -188,8 +189,7 @@ class RolesGridPageUI extends Component { }, { field: 'metadata', - name: intl.formatMessage({ - id: 'xpack.security.management.roles.reservedColumnName', + name: i18n.translate('xpack.security.management.roles.reservedColumnName', { defaultMessage: 'Reserved', }), sortable: ({ metadata }: Role) => Boolean(metadata && metadata._reserved), @@ -197,8 +197,7 @@ class RolesGridPageUI extends Component { align: 'right', description: reservedRoleDesc, render: (metadata: Role['metadata']) => { - const label = intl.formatMessage({ - id: 'xpack.security.management.roles.reservedRoleIconLabel', + const label = i18n.translate('xpack.security.management.roles.reservedRoleIconLabel', { defaultMessage: 'Reserved role', }); @@ -210,8 +209,7 @@ class RolesGridPageUI extends Component { }, }, { - name: intl.formatMessage({ - id: 'xpack.security.management.roles.actionsColumnName', + name: i18n.translate('xpack.security.management.roles.actionsColumnName', { defaultMessage: 'Actions', }), width: '150px', @@ -219,15 +217,10 @@ class RolesGridPageUI extends Component { { available: (role: Role) => !isReadOnlyRole(role), render: (role: Role) => { - const title = intl.formatMessage( - { - id: 'xpack.security.management.roles.editRoleActionName', - defaultMessage: `Edit {roleName}`, - }, - { - roleName: role.name, - } - ); + const title = i18n.translate('xpack.security.management.roles.editRoleActionName', { + defaultMessage: `Edit {roleName}`, + values: { roleName: role.name }, + }); return ( { { available: (role: Role) => !isReservedRole(role), render: (role: Role) => { - const title = intl.formatMessage( - { - id: 'xpack.security.management.roles.cloneRoleActionName', - defaultMessage: `Clone {roleName}`, - }, - { - roleName: role.name, - } - ); + const title = i18n.translate('xpack.security.management.roles.cloneRoleActionName', { + defaultMessage: `Clone {roleName}`, + values: { roleName: role.name }, + }); return ( { private async loadRoles() { try { - const roles = await RolesApi.getRoles(); + const roles = await this.props.rolesAPIClient.getRoles(); this.setState({ roles }); } catch (e) { if (_.get(e, 'body.statusCode') === 403) { this.setState({ permissionDenied: true }); } else { - toastNotifications.addDanger( - this.props.intl.formatMessage( - { - id: 'xpack.security.management.roles.fetchingRolesErrorMessage', - defaultMessage: 'Error fetching roles: {message}', - }, - { message: _.get(e, 'body.message', '') } - ) + this.props.notifications.toasts.addDanger( + i18n.translate('xpack.security.management.roles.fetchingRolesErrorMessage', { + defaultMessage: 'Error fetching roles: {message}', + values: { message: _.get(e, 'body.message', '') }, + }) ); } } @@ -339,5 +324,3 @@ class RolesGridPageUI extends Component { this.setState({ showDeleteConfirmation: false }); }; } - -export const RolesGridPage = injectI18n(RolesGridPageUI); diff --git a/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx b/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx new file mode 100644 index 00000000000000..48bc1a6580a93d --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx @@ -0,0 +1,160 @@ +/* + * 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 { licenseMock } from '../../../common/licensing/index.mock'; + +jest.mock('./roles_grid', () => ({ + RolesGridPage: (props: any) => `Roles Page: ${JSON.stringify(props)}`, +})); + +jest.mock('./edit_role', () => ({ + EditRolePage: (props: any) => `Role Edit Page: ${JSON.stringify(props)}`, +})); + +import { rolesManagementApp } from './roles_management_app'; + +import { coreMock } from '../../../../../../src/core/public/mocks'; + +async function mountApp(basePath: string) { + const { fatalErrors } = coreMock.createSetup(); + const container = document.createElement('div'); + const setBreadcrumbs = jest.fn(); + + const unmount = await rolesManagementApp + .create({ + license: licenseMock.create(), + fatalErrors, + getStartServices: jest.fn().mockResolvedValue([coreMock.createStart(), { data: {} }]), + }) + .mount({ basePath, element: container, setBreadcrumbs }); + + return { unmount, container, setBreadcrumbs }; +} + +describe('rolesManagementApp', () => { + it('create() returns proper management app descriptor', () => { + const { fatalErrors, getStartServices } = coreMock.createSetup(); + + expect( + rolesManagementApp.create({ + license: licenseMock.create(), + fatalErrors, + getStartServices: getStartServices as any, + }) + ).toMatchInlineSnapshot(` + Object { + "id": "roles", + "mount": [Function], + "order": 20, + "title": "Roles", + } + `); + }); + + it('mount() works for the `grid` page', async () => { + const basePath = '/some-base-path/roles'; + window.location.hash = basePath; + + const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: `#${basePath}`, text: 'Roles' }]); + expect(container).toMatchInlineSnapshot(` +
+ Roles Page: {"notifications":{"toasts":{}},"rolesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}}} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('mount() works for the `create role` page', async () => { + const basePath = '/some-base-path/roles'; + window.location.hash = `${basePath}/edit`; + + const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `#${basePath}`, text: 'Roles' }, + { text: 'Create' }, + ]); + expect(container).toMatchInlineSnapshot(` +
+ Role Edit Page: {"action":"edit","rolesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"userAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"http":{"basePath":{"basePath":""},"anonymousPaths":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}}} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('mount() works for the `edit role` page', async () => { + const basePath = '/some-base-path/roles'; + const roleName = 'someRoleName'; + window.location.hash = `${basePath}/edit/${roleName}`; + + const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `#${basePath}`, text: 'Roles' }, + { href: `#/some-base-path/roles/edit/${roleName}`, text: roleName }, + ]); + expect(container).toMatchInlineSnapshot(` +
+ Role Edit Page: {"action":"edit","roleName":"someRoleName","rolesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"userAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"http":{"basePath":{"basePath":""},"anonymousPaths":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}}} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('mount() works for the `clone role` page', async () => { + const basePath = '/some-base-path/roles'; + const roleName = 'someRoleName'; + window.location.hash = `${basePath}/clone/${roleName}`; + + const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `#${basePath}`, text: 'Roles' }, + { text: 'Create' }, + ]); + expect(container).toMatchInlineSnapshot(` +
+ Role Edit Page: {"action":"clone","roleName":"someRoleName","rolesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"userAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"http":{"basePath":{"basePath":""},"anonymousPaths":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}}} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('mount() properly encodes role name in `edit role` page link in breadcrumbs', async () => { + const basePath = '/some-base-path/roles'; + const roleName = 'some 安全性 role'; + window.location.hash = `${basePath}/edit/${roleName}`; + + const { setBreadcrumbs } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `#${basePath}`, text: 'Roles' }, + { + href: '#/some-base-path/roles/edit/some%20%E5%AE%89%E5%85%A8%E6%80%A7%20role', + text: roleName, + }, + ]); + }); +}); diff --git a/x-pack/plugins/security/public/management/roles/roles_management_app.tsx b/x-pack/plugins/security/public/management/roles/roles_management_app.tsx new file mode 100644 index 00000000000000..4e8c95b61c2f19 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/roles_management_app.tsx @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { HashRouter as Router, Route, Switch, useParams } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { CoreSetup, FatalErrorsSetup } from 'src/core/public'; +import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; +import { SecurityLicense } from '../../../common/licensing'; +import { PluginStartDependencies } from '../../plugin'; +import { UserAPIClient } from '../users'; +import { RolesAPIClient } from './roles_api_client'; +import { RolesGridPage } from './roles_grid'; +import { EditRolePage } from './edit_role'; +import { DocumentationLinksService } from './documentation_links'; +import { IndicesAPIClient } from './indices_api_client'; +import { PrivilegesAPIClient } from './privileges_api_client'; + +interface CreateParams { + fatalErrors: FatalErrorsSetup; + license: SecurityLicense; + getStartServices: CoreSetup['getStartServices']; +} + +export const rolesManagementApp = Object.freeze({ + id: 'roles', + create({ license, fatalErrors, getStartServices }: CreateParams) { + return { + id: this.id, + order: 20, + title: i18n.translate('xpack.security.management.rolesTitle', { defaultMessage: 'Roles' }), + async mount({ basePath, element, setBreadcrumbs }) { + const [ + { application, docLinks, http, i18n: i18nStart, injectedMetadata, notifications }, + { data }, + ] = await getStartServices(); + + const rolesBreadcrumbs = [ + { + text: i18n.translate('xpack.security.roles.breadcrumb', { defaultMessage: 'Roles' }), + href: `#${basePath}`, + }, + ]; + + const rolesAPIClient = new RolesAPIClient(http); + const RolesGridPageWithBreadcrumbs = () => { + setBreadcrumbs(rolesBreadcrumbs); + return ; + }; + + const EditRolePageWithBreadcrumbs = ({ action }: { action: 'edit' | 'clone' }) => { + const { roleName } = useParams<{ roleName?: string }>(); + + setBreadcrumbs([ + ...rolesBreadcrumbs, + action === 'edit' && roleName + ? { text: roleName, href: `#${basePath}/edit/${encodeURIComponent(roleName)}` } + : { + text: i18n.translate('xpack.security.roles.createBreadcrumb', { + defaultMessage: 'Create', + }), + }, + ]); + + return ( + + ); + }; + + render( + + + + + + + + + + + + + + + , + element + ); + + return () => { + unmountComponentAtNode(element); + }; + }, + } as RegisterManagementAppArgs; + }, +}); diff --git a/x-pack/plugins/security/public/management/users/_index.scss b/x-pack/plugins/security/public/management/users/_index.scss new file mode 100644 index 00000000000000..35df0c1b965837 --- /dev/null +++ b/x-pack/plugins/security/public/management/users/_index.scss @@ -0,0 +1 @@ +@import './edit_user/index'; diff --git a/x-pack/legacy/plugins/security/public/components/management/change_password_form/change_password_form.test.tsx b/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.test.tsx similarity index 82% rename from x-pack/legacy/plugins/security/public/components/management/change_password_form/change_password_form.test.tsx rename to x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.test.tsx index 221120532318cb..be46612767a593 100644 --- a/x-pack/legacy/plugins/security/public/components/management/change_password_form/change_password_form.test.tsx +++ b/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.test.tsx @@ -7,10 +7,12 @@ import { EuiFieldText } from '@elastic/eui'; import { ReactWrapper } from 'enzyme'; import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { User } from '../../../../common/model'; -import { UserAPIClient } from '../../../lib/api'; +import { User } from '../../../../../common/model'; import { ChangePasswordForm } from './change_password_form'; +import { coreMock } from '../../../../../../../../src/core/public/mocks'; +import { userAPIClientMock } from '../../index.mock'; + function getCurrentPasswordField(wrapper: ReactWrapper) { return wrapper.find(EuiFieldText).filter('[data-test-subj="currentPassword"]'); } @@ -23,8 +25,6 @@ function getConfirmPasswordField(wrapper: ReactWrapper) { return wrapper.find(EuiFieldText).filter('[data-test-subj="confirmNewPassword"]'); } -jest.mock('ui/kfetch'); - describe('', () => { describe('for the current user', () => { it('shows fields for current and new passwords', () => { @@ -40,7 +40,8 @@ describe('', () => { ); @@ -60,15 +61,15 @@ describe('', () => { const callback = jest.fn(); - const apiClient = new UserAPIClient(); - apiClient.changePassword = jest.fn(); + const apiClientMock = userAPIClientMock.create(); const wrapper = mountWithIntl( ); @@ -83,8 +84,8 @@ describe('', () => { wrapper.find('button[data-test-subj="changePasswordButton"]').simulate('click'); - expect(apiClient.changePassword).toHaveBeenCalledTimes(1); - expect(apiClient.changePassword).toHaveBeenCalledWith( + expect(apiClientMock.changePassword).toHaveBeenCalledTimes(1); + expect(apiClientMock.changePassword).toHaveBeenCalledWith( 'user', 'myNewPassword', 'myCurrentPassword' @@ -106,7 +107,8 @@ describe('', () => { ); diff --git a/x-pack/legacy/plugins/security/public/components/management/change_password_form/change_password_form.tsx b/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.tsx similarity index 96% rename from x-pack/legacy/plugins/security/public/components/management/change_password_form/change_password_form.tsx rename to x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.tsx index 61c0b77decd565..6dcf330ec6f9e1 100644 --- a/x-pack/legacy/plugins/security/public/components/management/change_password_form/change_password_form.tsx +++ b/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.tsx @@ -5,10 +5,7 @@ */ import { EuiButton, - // @ts-ignore EuiButtonEmpty, - // @ts-ignore - EuiDescribedFormGroup, EuiFieldText, EuiFlexGroup, EuiFlexItem, @@ -18,15 +15,16 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { ChangeEvent, Component } from 'react'; -import { toastNotifications } from 'ui/notify'; -import { User } from '../../../../common/model'; -import { UserAPIClient } from '../../../lib/api'; +import { NotificationsStart } from 'src/core/public'; +import { User } from '../../../../../common/model'; +import { UserAPIClient } from '../..'; interface Props { user: User; isUserChangingOwnPassword: boolean; onChangePassword?: () => void; - apiClient: UserAPIClient; + apiClient: PublicMethodsOf; + notifications: NotificationsStart; } interface State { @@ -294,7 +292,7 @@ export class ChangePasswordForm extends Component { }; private handleChangePasswordSuccess = () => { - toastNotifications.addSuccess({ + this.props.notifications.toasts.addSuccess({ title: i18n.translate('xpack.security.account.changePasswordSuccess', { defaultMessage: 'Your password has been changed.', }), @@ -317,7 +315,7 @@ export class ChangePasswordForm extends Component { if (error.body && error.body.statusCode === 403) { this.setState({ currentPasswordError: true }); } else { - toastNotifications.addDanger( + this.props.notifications.toasts.addDanger( i18n.translate('xpack.security.management.users.editUser.settingPasswordErrorMessage', { defaultMessage: 'Error setting password: {message}', values: { message: _.get(error, 'body.message') }, diff --git a/x-pack/legacy/plugins/security/public/components/management/change_password_form/index.ts b/x-pack/plugins/security/public/management/users/components/change_password_form/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/components/management/change_password_form/index.ts rename to x-pack/plugins/security/public/management/users/components/change_password_form/index.ts diff --git a/x-pack/legacy/plugins/security/public/components/management/users/confirm_delete.test.tsx b/x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.test.tsx similarity index 58% rename from x-pack/legacy/plugins/security/public/components/management/users/confirm_delete.test.tsx rename to x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.test.tsx index 9f69fc7a7551f0..bcec707b03f936 100644 --- a/x-pack/legacy/plugins/security/public/components/management/users/confirm_delete.test.tsx +++ b/x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.test.tsx @@ -5,16 +5,21 @@ */ import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { ConfirmDeleteUsers } from './confirm_delete'; +import { ConfirmDeleteUsers } from './confirm_delete_users'; import React from 'react'; -import { UserAPIClient } from '../../../lib/api'; -jest.mock('ui/kfetch'); +import { coreMock } from '../../../../../../../../src/core/public/mocks'; +import { userAPIClientMock } from '../../index.mock'; describe('ConfirmDeleteUsers', () => { it('renders a warning for a single user', () => { const wrapper = mountWithIntl( - + ); expect(wrapper.find('EuiModalHeaderTitle').text()).toMatchInlineSnapshot(`"Delete user foo"`); @@ -23,7 +28,8 @@ describe('ConfirmDeleteUsers', () => { it('renders a warning for a multiple users', () => { const wrapper = mountWithIntl( @@ -35,7 +41,12 @@ describe('ConfirmDeleteUsers', () => { it('fires onCancel when the operation is cancelled', () => { const onCancel = jest.fn(); const wrapper = mountWithIntl( - + ); expect(onCancel).toBeCalledTimes(0); @@ -47,50 +58,48 @@ describe('ConfirmDeleteUsers', () => { it('deletes the requested users when confirmed', () => { const onCancel = jest.fn(); - const deleteUser = jest.fn(); - - const apiClient = new UserAPIClient(); - apiClient.deleteUser = deleteUser; + const apiClientMock = userAPIClientMock.create(); const wrapper = mountWithIntl( ); wrapper.find('EuiButton[data-test-subj="confirmModalConfirmButton"]').simulate('click'); - expect(deleteUser).toBeCalledTimes(2); - expect(deleteUser).toBeCalledWith('foo'); - expect(deleteUser).toBeCalledWith('bar'); + expect(apiClientMock.deleteUser).toBeCalledTimes(2); + expect(apiClientMock.deleteUser).toBeCalledWith('foo'); + expect(apiClientMock.deleteUser).toBeCalledWith('bar'); }); it('attempts to delete all users even if some fail', () => { const onCancel = jest.fn(); - const deleteUser = jest.fn().mockImplementation(user => { + + const apiClientMock = userAPIClientMock.create(); + apiClientMock.deleteUser.mockImplementation(user => { if (user === 'foo') { return Promise.reject('something terrible happened'); } return Promise.resolve(); }); - const apiClient = new UserAPIClient(); - apiClient.deleteUser = deleteUser; - const wrapper = mountWithIntl( ); wrapper.find('EuiButton[data-test-subj="confirmModalConfirmButton"]').simulate('click'); - expect(deleteUser).toBeCalledTimes(2); - expect(deleteUser).toBeCalledWith('foo'); - expect(deleteUser).toBeCalledWith('bar'); + expect(apiClientMock.deleteUser).toBeCalledTimes(2); + expect(apiClientMock.deleteUser).toBeCalledWith('foo'); + expect(apiClientMock.deleteUser).toBeCalledWith('bar'); }); }); diff --git a/x-pack/legacy/plugins/security/public/components/management/users/confirm_delete.tsx b/x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.tsx similarity index 50% rename from x-pack/legacy/plugins/security/public/components/management/users/confirm_delete.tsx rename to x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.tsx index 53bb022afb513f..b7269e0168d7d5 100644 --- a/x-pack/legacy/plugins/security/public/components/management/users/confirm_delete.tsx +++ b/x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.tsx @@ -6,51 +6,46 @@ import React, { Component, Fragment } from 'react'; import { EuiOverlayMask, EuiConfirmModal } from '@elastic/eui'; -import { toastNotifications } from 'ui/notify'; -import { FormattedMessage, injectI18n, InjectedIntl } from '@kbn/i18n/react'; -import { UserAPIClient } from '../../../lib/api'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { NotificationsStart } from 'src/core/public'; +import { UserAPIClient } from '../..'; interface Props { - intl: InjectedIntl; usersToDelete: string[]; - apiClient: UserAPIClient; + apiClient: PublicMethodsOf; + notifications: NotificationsStart; onCancel: () => void; callback?: (usersToDelete: string[], errors: string[]) => void; } -class ConfirmDeleteUI extends Component { +export class ConfirmDeleteUsers extends Component { public render() { - const { usersToDelete, onCancel, intl } = this.props; + const { usersToDelete, onCancel } = this.props; const moreThanOne = usersToDelete.length > 1; const title = moreThanOne - ? intl.formatMessage( - { - id: 'xpack.security.management.users.confirmDelete.deleteMultipleUsersTitle', - defaultMessage: 'Delete {userLength} users', - }, - { userLength: usersToDelete.length } - ) - : intl.formatMessage( - { - id: 'xpack.security.management.users.confirmDelete.deleteOneUserTitle', - defaultMessage: 'Delete user {userLength}', - }, - { userLength: usersToDelete[0] } - ); + ? i18n.translate('xpack.security.management.users.confirmDelete.deleteMultipleUsersTitle', { + defaultMessage: 'Delete {userLength} users', + values: { userLength: usersToDelete.length }, + }) + : i18n.translate('xpack.security.management.users.confirmDelete.deleteOneUserTitle', { + defaultMessage: 'Delete user {userLength}', + values: { userLength: usersToDelete[0] }, + }); return (
@@ -82,31 +77,23 @@ class ConfirmDeleteUI extends Component { } private deleteUsers = () => { - const { usersToDelete, callback, apiClient } = this.props; + const { usersToDelete, callback, apiClient, notifications } = this.props; const errors: string[] = []; usersToDelete.forEach(async username => { try { await apiClient.deleteUser(username); - toastNotifications.addSuccess( - this.props.intl.formatMessage( - { - id: - 'xpack.security.management.users.confirmDelete.userSuccessfullyDeletedNotificationMessage', - defaultMessage: 'Deleted user {username}', - }, - { username } + notifications.toasts.addSuccess( + i18n.translate( + 'xpack.security.management.users.confirmDelete.userSuccessfullyDeletedNotificationMessage', + { defaultMessage: 'Deleted user {username}', values: { username } } ) ); } catch (e) { errors.push(username); - toastNotifications.addDanger( - this.props.intl.formatMessage( - { - id: - 'xpack.security.management.users.confirmDelete.userDeletingErrorNotificationMessage', - defaultMessage: 'Error deleting user {username}', - }, - { username } + notifications.toasts.addDanger( + i18n.translate( + 'xpack.security.management.users.confirmDelete.userDeletingErrorNotificationMessage', + { defaultMessage: 'Error deleting user {username}', values: { username } } ) ); } @@ -116,5 +103,3 @@ class ConfirmDeleteUI extends Component { }); }; } - -export const ConfirmDeleteUsers = injectI18n(ConfirmDeleteUI); diff --git a/x-pack/legacy/plugins/security/public/components/management/users/index.ts b/x-pack/plugins/security/public/management/users/components/confirm_delete_users/index.ts similarity index 79% rename from x-pack/legacy/plugins/security/public/components/management/users/index.ts rename to x-pack/plugins/security/public/management/users/components/confirm_delete_users/index.ts index 813e671c05ccf2..fde35ab0f0d02e 100644 --- a/x-pack/legacy/plugins/security/public/components/management/users/index.ts +++ b/x-pack/plugins/security/public/management/users/components/confirm_delete_users/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { ConfirmDeleteUsers } from './confirm_delete'; +export { ConfirmDeleteUsers } from './confirm_delete_users'; diff --git a/x-pack/plugins/security/public/management/users/components/index.ts b/x-pack/plugins/security/public/management/users/components/index.ts new file mode 100644 index 00000000000000..54011a6a24cbd2 --- /dev/null +++ b/x-pack/plugins/security/public/management/users/components/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 { ChangePasswordForm } from './change_password_form'; +export { ConfirmDeleteUsers } from './confirm_delete_users'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_user/_users.scss b/x-pack/plugins/security/public/management/users/edit_user/_edit_user_page.scss similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_user/_users.scss rename to x-pack/plugins/security/public/management/users/edit_user/_edit_user_page.scss diff --git a/x-pack/plugins/security/public/management/users/edit_user/_index.scss b/x-pack/plugins/security/public/management/users/edit_user/_index.scss new file mode 100644 index 00000000000000..734ba7882ba72c --- /dev/null +++ b/x-pack/plugins/security/public/management/users/edit_user/_index.scss @@ -0,0 +1 @@ +@import './edit_user_page'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.test.tsx b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx similarity index 70% rename from x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.test.tsx rename to x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx index 639646ce48e224..543d20bb92afe8 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.test.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx @@ -8,13 +8,14 @@ import { act } from '@testing-library/react'; import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; import { EditUserPage } from './edit_user_page'; import React from 'react'; -import { securityMock } from '../../../../../../../../plugins/security/public/mocks'; -import { UserAPIClient } from '../../../../lib/api'; -import { User, Role } from '../../../../../common/model'; +import { User, Role } from '../../../../common/model'; import { ReactWrapper } from 'enzyme'; -import { mockAuthenticatedUser } from '../../../../../../../../plugins/security/common/model/authenticated_user.mock'; -jest.mock('ui/kfetch'); +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { mockAuthenticatedUser } from '../../../../common/model/authenticated_user.mock'; +import { securityMock } from '../../../mocks'; +import { rolesAPIClientMock } from '../../roles/index.mock'; +import { userAPIClientMock } from '../index.mock'; const createUser = (username: string) => { const user: User = { @@ -34,14 +35,12 @@ const createUser = (username: string) => { return user; }; -const buildClient = () => { - const apiClient = new UserAPIClient(); +const buildClients = () => { + const apiClient = userAPIClientMock.create(); + apiClient.getUser.mockImplementation(async (username: string) => createUser(username)); - apiClient.getUser = jest - .fn() - .mockImplementation(async (username: string) => createUser(username)); - - apiClient.getRoles = jest.fn().mockImplementation(() => { + const rolesAPIClient = rolesAPIClientMock.create(); + rolesAPIClient.getRoles.mockImplementation(() => { return Promise.resolve([ { name: 'role 1', @@ -64,7 +63,7 @@ const buildClient = () => { ] as Role[]); }); - return apiClient; + return { apiClient, rolesAPIClient }; }; function buildSecuritySetup() { @@ -85,15 +84,15 @@ function expectMissingSaveButton(wrapper: ReactWrapper) { describe('EditUserPage', () => { it('allows reserved users to be viewed', async () => { - const apiClient = buildClient(); + const { apiClient, rolesAPIClient } = buildClients(); const securitySetup = buildSecuritySetup(); const wrapper = mountWithIntl( - path} - intl={null as any} + rolesAPIClient={rolesAPIClient} + authc={securitySetup.authc} + notifications={coreMock.createStart().notifications} /> ); @@ -106,15 +105,15 @@ describe('EditUserPage', () => { }); it('allows new users to be created', async () => { - const apiClient = buildClient(); + const { apiClient, rolesAPIClient } = buildClients(); const securitySetup = buildSecuritySetup(); const wrapper = mountWithIntl( - path} - intl={null as any} + rolesAPIClient={rolesAPIClient} + authc={securitySetup.authc} + notifications={coreMock.createStart().notifications} /> ); @@ -127,15 +126,15 @@ describe('EditUserPage', () => { }); it('allows existing users to be edited', async () => { - const apiClient = buildClient(); + const { apiClient, rolesAPIClient } = buildClients(); const securitySetup = buildSecuritySetup(); const wrapper = mountWithIntl( - path} - intl={null as any} + rolesAPIClient={rolesAPIClient} + authc={securitySetup.authc} + notifications={coreMock.createStart().notifications} /> ); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.tsx b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx similarity index 77% rename from x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.tsx rename to x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx index bbffe07485f8dc..576f3ff9e6008c 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx @@ -26,22 +26,23 @@ import { EuiHorizontalRule, EuiSpacer, } from '@elastic/eui'; -import { toastNotifications } from 'ui/notify'; -import { FormattedMessage, injectI18n, InjectedIntl } from '@kbn/i18n/react'; -import { SecurityPluginSetup } from '../../../../../../../../plugins/security/public'; -import { UserValidator, UserValidationResult } from '../../../../lib/validate_user'; -import { User, EditUser, Role } from '../../../../../common/model'; -import { USERS_PATH } from '../../../../views/management/management_urls'; -import { ConfirmDeleteUsers } from '../../../../components/management/users'; -import { UserAPIClient } from '../../../../lib/api'; -import { ChangePasswordForm } from '../../../../components/management/change_password_form'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { NotificationsStart } from 'src/core/public'; +import { User, EditUser, Role } from '../../../../common/model'; +import { AuthenticationServiceSetup } from '../../../authentication'; +import { USERS_PATH } from '../../management_urls'; +import { RolesAPIClient } from '../../roles'; +import { ConfirmDeleteUsers, ChangePasswordForm } from '../components'; +import { UserValidator, UserValidationResult } from './validate_user'; +import { UserAPIClient } from '..'; interface Props { - username: string; - intl: InjectedIntl; - changeUrl: (path: string) => void; - apiClient: UserAPIClient; - securitySetup: SecurityPluginSetup; + username?: string; + apiClient: PublicMethodsOf; + rolesAPIClient: PublicMethodsOf; + authc: AuthenticationServiceSetup; + notifications: NotificationsStart; } interface State { @@ -56,7 +57,11 @@ interface State { formError: UserValidationResult | null; } -class EditUserPageUI extends Component { +function backToUserList() { + window.location.hash = USERS_PATH; +} + +export class EditUserPage extends Component { private validator: UserValidator; constructor(props: Props) { @@ -84,7 +89,17 @@ class EditUserPageUI extends Component { } public async componentDidMount() { - const { username, apiClient, securitySetup } = this.props; + await this.setCurrentUser(); + } + + public async componentDidUpdate(prevProps: Props) { + if (prevProps.username !== this.props.username) { + await this.setCurrentUser(); + } + } + + private async setCurrentUser() { + const { username, apiClient, rolesAPIClient, notifications, authc } = this.props; let { user, currentUser } = this.state; if (username) { try { @@ -93,26 +108,24 @@ class EditUserPageUI extends Component { password: '', confirmPassword: '', }; - currentUser = await securitySetup.authc.getCurrentUser(); + currentUser = await authc.getCurrentUser(); } catch (err) { - toastNotifications.addDanger({ - title: this.props.intl.formatMessage({ - id: 'xpack.security.management.users.editUser.errorLoadingUserTitle', + notifications.toasts.addDanger({ + title: i18n.translate('xpack.security.management.users.editUser.errorLoadingUserTitle', { defaultMessage: 'Error loading user', }), text: get(err, 'body.message') || err.message, }); - return; + return backToUserList(); } } let roles: Role[] = []; try { - roles = await apiClient.getRoles(); + roles = await rolesAPIClient.getRoles(); } catch (err) { - toastNotifications.addDanger({ - title: this.props.intl.formatMessage({ - id: 'xpack.security.management.users.editUser.errorLoadingRolesTitle', + notifications.toasts.addDanger({ + title: i18n.translate('xpack.security.management.users.editUser.errorLoadingRolesTitle', { defaultMessage: 'Error loading roles', }), text: get(err, 'body.message') || err.message, @@ -131,8 +144,7 @@ class EditUserPageUI extends Component { private handleDelete = (usernames: string[], errors: string[]) => { if (errors.length === 0) { - const { changeUrl } = this.props; - changeUrl(USERS_PATH); + backToUserList(); } }; @@ -148,7 +160,7 @@ class EditUserPageUI extends Component { this.setState({ formError: null, }); - const { changeUrl, apiClient } = this.props; + const { apiClient } = this.props; const { user, isNewUser, selectedRoles } = this.state; const userToSave: EditUser = { ...user }; if (!isNewUser) { @@ -160,26 +172,23 @@ class EditUserPageUI extends Component { }); try { await apiClient.saveUser(userToSave); - toastNotifications.addSuccess( - this.props.intl.formatMessage( + this.props.notifications.toasts.addSuccess( + i18n.translate( + 'xpack.security.management.users.editUser.userSuccessfullySavedNotificationMessage', { - id: - 'xpack.security.management.users.editUser.userSuccessfullySavedNotificationMessage', defaultMessage: 'Saved user {message}', - }, - { message: user.username } + values: { message: user.username }, + } ) ); - changeUrl(USERS_PATH); + + backToUserList(); } catch (e) { - toastNotifications.addDanger( - this.props.intl.formatMessage( - { - id: 'xpack.security.management.users.editUser.savingUserErrorMessage', - defaultMessage: 'Error saving user: {message}', - }, - { message: get(e, 'body.message', 'Unknown error') } - ) + this.props.notifications.toasts.addDanger( + i18n.translate('xpack.security.management.users.editUser.savingUserErrorMessage', { + defaultMessage: 'Error saving user: {message}', + values: { message: get(e, 'body.message', 'Unknown error') }, + }) ); } } @@ -189,8 +198,7 @@ class EditUserPageUI extends Component { return ( { /> { {user.username === 'kibana' ? ( @@ -260,6 +268,7 @@ class EditUserPageUI extends Component { isUserChangingOwnPassword={userIsLoggedInUser} onChangePassword={this.toggleChangePasswordForm} apiClient={this.props.apiClient} + notifications={this.props.notifications} /> ); @@ -352,7 +361,6 @@ class EditUserPageUI extends Component { }; public render() { - const { changeUrl, intl } = this.props; const { user, roles, @@ -417,6 +425,7 @@ class EditUserPageUI extends Component { usersToDelete={[user.username]} callback={this.handleDelete} apiClient={this.props.apiClient} + notifications={this.props.notifications} /> ) : null} @@ -425,17 +434,16 @@ class EditUserPageUI extends Component { {...this.validator.validateUsername(this.state.user)} helpText={ !isNewUser && !reserved - ? intl.formatMessage({ - id: - 'xpack.security.management.users.editUser.changingUserNameAfterCreationDescription', - defaultMessage: `Usernames can't be changed after creation.`, - }) + ? i18n.translate( + 'xpack.security.management.users.editUser.changingUserNameAfterCreationDescription', + { defaultMessage: `Usernames can't be changed after creation.` } + ) : null } - label={intl.formatMessage({ - id: 'xpack.security.management.users.editUser.usernameFormRowLabel', - defaultMessage: 'Username', - })} + label={i18n.translate( + 'xpack.security.management.users.editUser.usernameFormRowLabel', + { defaultMessage: 'Username' } + )} > { {reserved ? null : ( { { )} { @@ -513,7 +521,7 @@ class EditUserPageUI extends Component { {reserved && ( - changeUrl(USERS_PATH)}> + { - changeUrl(USERS_PATH)} - > + { ); } } - -export const EditUserPage = injectI18n(EditUserPageUI); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_user/components/index.ts b/x-pack/plugins/security/public/management/users/edit_user/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_user/components/index.ts rename to x-pack/plugins/security/public/management/users/edit_user/index.ts diff --git a/x-pack/legacy/plugins/security/public/lib/validate_user.test.ts b/x-pack/plugins/security/public/management/users/edit_user/validate_user.test.ts similarity index 98% rename from x-pack/legacy/plugins/security/public/lib/validate_user.test.ts rename to x-pack/plugins/security/public/management/users/edit_user/validate_user.test.ts index 0535248fede88d..6050e1868a759a 100644 --- a/x-pack/legacy/plugins/security/public/lib/validate_user.test.ts +++ b/x-pack/plugins/security/public/management/users/edit_user/validate_user.test.ts @@ -5,7 +5,7 @@ */ import { UserValidator, UserValidationResult } from './validate_user'; -import { User, EditUser } from '../../common/model'; +import { User, EditUser } from '../../../../common/model'; function expectValid(result: UserValidationResult) { expect(result.isInvalid).toBe(false); diff --git a/x-pack/legacy/plugins/security/public/lib/validate_user.ts b/x-pack/plugins/security/public/management/users/edit_user/validate_user.ts similarity index 98% rename from x-pack/legacy/plugins/security/public/lib/validate_user.ts rename to x-pack/plugins/security/public/management/users/edit_user/validate_user.ts index 113aaacdcbf966..5edd96c68bf0dc 100644 --- a/x-pack/legacy/plugins/security/public/lib/validate_user.ts +++ b/x-pack/plugins/security/public/management/users/edit_user/validate_user.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { User, EditUser } from '../../common/model'; +import { User, EditUser } from '../../../../common/model'; interface UserValidatorOptions { shouldValidate?: boolean; diff --git a/x-pack/legacy/plugins/security/public/views/management/index.js b/x-pack/plugins/security/public/management/users/index.mock.ts similarity index 80% rename from x-pack/legacy/plugins/security/public/views/management/index.js rename to x-pack/plugins/security/public/management/users/index.mock.ts index 0ed6fe09ef80a4..f090f88da500de 100644 --- a/x-pack/legacy/plugins/security/public/views/management/index.js +++ b/x-pack/plugins/security/public/management/users/index.mock.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import './management'; +export { userAPIClientMock } from './user_api_client.mock'; diff --git a/x-pack/legacy/plugins/security/public/objects/index.ts b/x-pack/plugins/security/public/management/users/index.ts similarity index 68% rename from x-pack/legacy/plugins/security/public/objects/index.ts rename to x-pack/plugins/security/public/management/users/index.ts index a6238ca879901c..c8b4d41973da6a 100644 --- a/x-pack/legacy/plugins/security/public/objects/index.ts +++ b/x-pack/plugins/security/public/management/users/index.ts @@ -4,6 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { saveRole, deleteRole } from './lib/roles'; - -export { getFields } from './lib/get_fields'; +export { UserAPIClient } from './user_api_client'; +export { usersManagementApp } from './users_management_app'; diff --git a/x-pack/plugins/security/public/management/users/user_api_client.mock.ts b/x-pack/plugins/security/public/management/users/user_api_client.mock.ts new file mode 100644 index 00000000000000..7223f78d57fdc0 --- /dev/null +++ b/x-pack/plugins/security/public/management/users/user_api_client.mock.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. + */ + +export const userAPIClientMock = { + create: () => ({ + getUsers: jest.fn(), + getUser: jest.fn(), + deleteUser: jest.fn(), + saveUser: jest.fn(), + changePassword: jest.fn(), + }), +}; diff --git a/x-pack/plugins/security/public/management/users/user_api_client.ts b/x-pack/plugins/security/public/management/users/user_api_client.ts new file mode 100644 index 00000000000000..61dd09d2c5e3de --- /dev/null +++ b/x-pack/plugins/security/public/management/users/user_api_client.ts @@ -0,0 +1,45 @@ +/* + * 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 { HttpStart } from 'src/core/public'; +import { User, EditUser } from '../../../common/model'; + +const usersUrl = '/internal/security/users'; + +export class UserAPIClient { + constructor(private readonly http: HttpStart) {} + + public async getUsers() { + return await this.http.get(usersUrl); + } + + public async getUser(username: string) { + return await this.http.get(`${usersUrl}/${encodeURIComponent(username)}`); + } + + public async deleteUser(username: string) { + await this.http.delete(`${usersUrl}/${encodeURIComponent(username)}`); + } + + public async saveUser(user: EditUser) { + await this.http.post(`${usersUrl}/${encodeURIComponent(user.username)}`, { + body: JSON.stringify(user), + }); + } + + public async changePassword(username: string, password: string, currentPassword: string) { + const data: Record = { + newPassword: password, + }; + if (currentPassword) { + data.password = currentPassword; + } + + await this.http.post(`${usersUrl}/${encodeURIComponent(username)}/password`, { + body: JSON.stringify(data), + }); + } +} diff --git a/x-pack/legacy/plugins/security/public/views/management/users_grid/components/index.ts b/x-pack/plugins/security/public/management/users/users_grid/index.ts similarity index 82% rename from x-pack/legacy/plugins/security/public/views/management/users_grid/components/index.ts rename to x-pack/plugins/security/public/management/users/users_grid/index.ts index 03721f5ce93b1f..90e16248e19c36 100644 --- a/x-pack/legacy/plugins/security/public/views/management/users_grid/components/index.ts +++ b/x-pack/plugins/security/public/management/users/users_grid/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { UsersListPage } from './users_list_page'; +export { UsersGridPage } from './users_grid_page'; diff --git a/x-pack/legacy/plugins/security/public/views/management/users_grid/components/users_list_page.test.tsx b/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.test.tsx similarity index 63% rename from x-pack/legacy/plugins/security/public/views/management/users_grid/components/users_list_page.test.tsx rename to x-pack/plugins/security/public/management/users/users_grid/users_grid_page.test.tsx index bdc0df9bae67c3..def0649953437d 100644 --- a/x-pack/legacy/plugins/security/public/views/management/users_grid/components/users_list_page.test.tsx +++ b/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.test.tsx @@ -4,19 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { UserAPIClient } from '../../../../lib/api'; -import { User } from '../../../../../common/model'; +import { User } from '../../../../common/model'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { UsersListPage } from './users_list_page'; +import { UsersGridPage } from './users_grid_page'; import React from 'react'; import { ReactWrapper } from 'enzyme'; +import { userAPIClientMock } from '../index.mock'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; -jest.mock('ui/kfetch'); - -describe('UsersListPage', () => { +describe('UsersGridPage', () => { it('renders the list of users', async () => { - const apiClient = new UserAPIClient(); - apiClient.getUsers = jest.fn().mockImplementation(() => { + const apiClientMock = userAPIClientMock.create(); + apiClientMock.getUsers.mockImplementation(() => { return Promise.resolve([ { username: 'foo', @@ -38,22 +37,27 @@ describe('UsersListPage', () => { ]); }); - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl( + + ); await waitForRender(wrapper); - expect(apiClient.getUsers).toBeCalledTimes(1); + expect(apiClientMock.getUsers).toBeCalledTimes(1); expect(wrapper.find('EuiInMemoryTable')).toHaveLength(1); expect(wrapper.find('EuiTableRow')).toHaveLength(2); }); it('renders a forbidden message if user is not authorized', async () => { - const apiClient = new UserAPIClient(); - apiClient.getUsers = jest.fn().mockImplementation(() => { - return Promise.reject({ body: { statusCode: 403 } }); - }); + const apiClient = userAPIClientMock.create(); + apiClient.getUsers.mockRejectedValue({ body: { statusCode: 403 } }); - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl( + + ); await waitForRender(wrapper); diff --git a/x-pack/legacy/plugins/security/public/views/management/users_grid/components/users_list_page.tsx b/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx similarity index 85% rename from x-pack/legacy/plugins/security/public/views/management/users_grid/components/users_list_page.tsx rename to x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx index df8522e5f32f91..fa15c3388fcc99 100644 --- a/x-pack/legacy/plugins/security/public/views/management/users_grid/components/users_list_page.tsx +++ b/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx @@ -19,15 +19,16 @@ import { EuiEmptyPrompt, EuiBasicTableColumn, } from '@elastic/eui'; -import { toastNotifications } from 'ui/notify'; -import { injectI18n, FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; -import { ConfirmDeleteUsers } from '../../../../components/management/users'; -import { User } from '../../../../../common/model'; -import { UserAPIClient } from '../../../../lib/api'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { NotificationsStart } from 'src/core/public'; +import { User } from '../../../../common/model'; +import { ConfirmDeleteUsers } from '../components'; +import { UserAPIClient } from '..'; interface Props { - intl: InjectedIntl; - apiClient: UserAPIClient; + apiClient: PublicMethodsOf; + notifications: NotificationsStart; } interface State { @@ -38,7 +39,7 @@ interface State { filter: string; } -class UsersListPageUI extends Component { +export class UsersGridPage extends Component { constructor(props: Props) { super(props); this.state = { @@ -56,7 +57,6 @@ class UsersListPageUI extends Component { public render() { const { users, filter, permissionDenied, showDeleteConfirmation, selection } = this.state; - const { intl } = this.props; if (permissionDenied) { return ( @@ -88,8 +88,7 @@ class UsersListPageUI extends Component { const columns: Array> = [ { field: 'full_name', - name: intl.formatMessage({ - id: 'xpack.security.management.users.fullNameColumnName', + name: i18n.translate('xpack.security.management.users.fullNameColumnName', { defaultMessage: 'Full Name', }), sortable: true, @@ -100,8 +99,7 @@ class UsersListPageUI extends Component { }, { field: 'username', - name: intl.formatMessage({ - id: 'xpack.security.management.users.userNameColumnName', + name: i18n.translate('xpack.security.management.users.userNameColumnName', { defaultMessage: 'User Name', }), sortable: true, @@ -114,8 +112,7 @@ class UsersListPageUI extends Component { }, { field: 'email', - name: intl.formatMessage({ - id: 'xpack.security.management.users.emailAddressColumnName', + name: i18n.translate('xpack.security.management.users.emailAddressColumnName', { defaultMessage: 'Email Address', }), sortable: true, @@ -126,8 +123,7 @@ class UsersListPageUI extends Component { }, { field: 'roles', - name: intl.formatMessage({ - id: 'xpack.security.management.users.rolesColumnName', + name: i18n.translate('xpack.security.management.users.rolesColumnName', { defaultMessage: 'Roles', }), render: (rolenames: string[]) => { @@ -144,15 +140,13 @@ class UsersListPageUI extends Component { }, { field: 'metadata', - name: intl.formatMessage({ - id: 'xpack.security.management.users.reservedColumnName', + name: i18n.translate('xpack.security.management.users.reservedColumnName', { defaultMessage: 'Reserved', }), sortable: ({ metadata }: User) => Boolean(metadata && metadata._reserved), width: '100px', align: 'right', - description: intl.formatMessage({ - id: 'xpack.security.management.users.reservedColumnDescription', + description: i18n.translate('xpack.security.management.users.reservedColumnDescription', { defaultMessage: 'Reserved users are built-in and cannot be removed. Only the password can be changed.', }), @@ -233,6 +227,7 @@ class UsersListPageUI extends Component { usersToDelete={selection.map(user => user.username)} callback={this.handleDelete} apiClient={this.props.apiClient} + notifications={this.props.notifications} /> ) : null} @@ -275,14 +270,11 @@ class UsersListPageUI extends Component { if (e.body.statusCode === 403) { this.setState({ permissionDenied: true }); } else { - toastNotifications.addDanger( - this.props.intl.formatMessage( - { - id: 'xpack.security.management.users.fetchingUsersErrorMessage', - defaultMessage: 'Error fetching users: {message}', - }, - { message: e.body.message } - ) + this.props.notifications.toasts.addDanger( + i18n.translate('xpack.security.management.users.fetchingUsersErrorMessage', { + defaultMessage: 'Error fetching users: {message}', + values: { message: e.body.message }, + }) ); } } @@ -315,5 +307,3 @@ class UsersListPageUI extends Component { this.setState({ showDeleteConfirmation: false }); }; } - -export const UsersListPage = injectI18n(UsersListPageUI); diff --git a/x-pack/plugins/security/public/management/users/users_management_app.test.tsx b/x-pack/plugins/security/public/management/users/users_management_app.test.tsx new file mode 100644 index 00000000000000..48ffcfc550a849 --- /dev/null +++ b/x-pack/plugins/security/public/management/users/users_management_app.test.tsx @@ -0,0 +1,131 @@ +/* + * 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. + */ + +jest.mock('./users_grid', () => ({ + UsersGridPage: (props: any) => `Users Page: ${JSON.stringify(props)}`, +})); + +jest.mock('./edit_user', () => ({ + EditUserPage: (props: any) => `User Edit Page: ${JSON.stringify(props)}`, +})); + +import { usersManagementApp } from './users_management_app'; + +import { coreMock } from '../../../../../../src/core/public/mocks'; +import { securityMock } from '../../mocks'; + +async function mountApp(basePath: string) { + const container = document.createElement('div'); + const setBreadcrumbs = jest.fn(); + + const unmount = await usersManagementApp + .create({ + authc: securityMock.createSetup().authc, + getStartServices: coreMock.createSetup().getStartServices as any, + }) + .mount({ basePath, element: container, setBreadcrumbs }); + + return { unmount, container, setBreadcrumbs }; +} + +describe('usersManagementApp', () => { + it('create() returns proper management app descriptor', () => { + expect( + usersManagementApp.create({ + authc: securityMock.createSetup().authc, + getStartServices: coreMock.createSetup().getStartServices as any, + }) + ).toMatchInlineSnapshot(` + Object { + "id": "users", + "mount": [Function], + "order": 10, + "title": "Users", + } + `); + }); + + it('mount() works for the `grid` page', async () => { + const basePath = '/some-base-path/users'; + window.location.hash = basePath; + + const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: `#${basePath}`, text: 'Users' }]); + expect(container).toMatchInlineSnapshot(` +
+ Users Page: {"notifications":{"toasts":{}},"apiClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}}} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('mount() works for the `create user` page', async () => { + const basePath = '/some-base-path/users'; + window.location.hash = `${basePath}/edit`; + + const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `#${basePath}`, text: 'Users' }, + { text: 'Create' }, + ]); + expect(container).toMatchInlineSnapshot(` +
+ User Edit Page: {"authc":{},"apiClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"notifications":{"toasts":{}}} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('mount() works for the `edit user` page', async () => { + const basePath = '/some-base-path/users'; + const userName = 'someUserName'; + window.location.hash = `${basePath}/edit/${userName}`; + + const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `#${basePath}`, text: 'Users' }, + { href: `#/some-base-path/users/edit/${userName}`, text: userName }, + ]); + expect(container).toMatchInlineSnapshot(` +
+ User Edit Page: {"authc":{},"apiClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"notifications":{"toasts":{}},"username":"someUserName"} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('mount() properly encodes user name in `edit user` page link in breadcrumbs', async () => { + const basePath = '/some-base-path/users'; + const username = 'some 安全性 user'; + window.location.hash = `${basePath}/edit/${username}`; + + const { setBreadcrumbs } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `#${basePath}`, text: 'Users' }, + { + href: '#/some-base-path/users/edit/some%20%E5%AE%89%E5%85%A8%E6%80%A7%20user', + text: username, + }, + ]); + }); +}); diff --git a/x-pack/plugins/security/public/management/users/users_management_app.tsx b/x-pack/plugins/security/public/management/users/users_management_app.tsx new file mode 100644 index 00000000000000..9aebb396ce9a92 --- /dev/null +++ b/x-pack/plugins/security/public/management/users/users_management_app.tsx @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { HashRouter as Router, Route, Switch, useParams } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { CoreSetup } from 'src/core/public'; +import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; +import { AuthenticationServiceSetup } from '../../authentication'; +import { PluginStartDependencies } from '../../plugin'; +import { RolesAPIClient } from '../roles'; +import { UserAPIClient } from './user_api_client'; +import { UsersGridPage } from './users_grid'; +import { EditUserPage } from './edit_user'; + +interface CreateParams { + authc: AuthenticationServiceSetup; + getStartServices: CoreSetup['getStartServices']; +} + +export const usersManagementApp = Object.freeze({ + id: 'users', + create({ authc, getStartServices }: CreateParams) { + return { + id: this.id, + order: 10, + title: i18n.translate('xpack.security.management.usersTitle', { defaultMessage: 'Users' }), + async mount({ basePath, element, setBreadcrumbs }) { + const [{ http, notifications, i18n: i18nStart }] = await getStartServices(); + const usersBreadcrumbs = [ + { + text: i18n.translate('xpack.security.users.breadcrumb', { defaultMessage: 'Users' }), + href: `#${basePath}`, + }, + ]; + + const userAPIClient = new UserAPIClient(http); + const UsersGridPageWithBreadcrumbs = () => { + setBreadcrumbs(usersBreadcrumbs); + return ; + }; + + const EditUserPageWithBreadcrumbs = () => { + const { username } = useParams<{ username?: string }>(); + + setBreadcrumbs([ + ...usersBreadcrumbs, + username + ? { text: username, href: `#${basePath}/edit/${encodeURIComponent(username)}` } + : { + text: i18n.translate('xpack.security.users.createBreadcrumb', { + defaultMessage: 'Create', + }), + }, + ]); + + return ( + + ); + }; + + render( + + + + + + + + + + + + , + element + ); + + return () => { + unmountComponentAtNode(element); + }; + }, + } as RegisterManagementAppArgs; + }, +}); diff --git a/x-pack/plugins/security/public/plugin.ts b/x-pack/plugins/security/public/plugin.ts deleted file mode 100644 index 50e0b838c750fc..00000000000000 --- a/x-pack/plugins/security/public/plugin.ts +++ /dev/null @@ -1,74 +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 { Plugin, CoreSetup, CoreStart } from 'src/core/public'; -import { LicensingPluginSetup } from '../../licensing/public'; -import { - SessionExpired, - SessionTimeout, - ISessionTimeout, - SessionTimeoutHttpInterceptor, - UnauthorizedResponseHttpInterceptor, -} from './session'; -import { SecurityLicenseService } from '../common/licensing'; -import { SecurityNavControlService } from './nav_control'; -import { AuthenticationService } from './authentication'; - -export interface PluginSetupDependencies { - licensing: LicensingPluginSetup; -} - -export class SecurityPlugin implements Plugin { - private sessionTimeout!: ISessionTimeout; - - private navControlService!: SecurityNavControlService; - - private securityLicenseService!: SecurityLicenseService; - - public setup(core: CoreSetup, { licensing }: PluginSetupDependencies) { - const { http, notifications, injectedMetadata } = core; - const { basePath, anonymousPaths } = http; - anonymousPaths.register('/login'); - anonymousPaths.register('/logout'); - anonymousPaths.register('/logged_out'); - - const tenant = `${injectedMetadata.getInjectedVar('session.tenant', '')}`; - const sessionExpired = new SessionExpired(basePath, tenant); - http.intercept(new UnauthorizedResponseHttpInterceptor(sessionExpired, anonymousPaths)); - this.sessionTimeout = new SessionTimeout(notifications, sessionExpired, http, tenant); - http.intercept(new SessionTimeoutHttpInterceptor(this.sessionTimeout, anonymousPaths)); - - this.navControlService = new SecurityNavControlService(); - this.securityLicenseService = new SecurityLicenseService(); - const { license } = this.securityLicenseService.setup({ license$: licensing.license$ }); - - const authc = new AuthenticationService().setup({ http: core.http }); - - this.navControlService.setup({ - securityLicense: license, - authc, - }); - - return { - authc, - sessionTimeout: this.sessionTimeout, - }; - } - - public start(core: CoreStart) { - this.sessionTimeout.start(); - this.navControlService.start({ core }); - } - - public stop() { - this.sessionTimeout.stop(); - this.navControlService.stop(); - this.securityLicenseService.stop(); - } -} - -export type SecurityPluginSetup = ReturnType; -export type SecurityPluginStart = ReturnType; diff --git a/x-pack/plugins/security/public/plugin.tsx b/x-pack/plugins/security/public/plugin.tsx new file mode 100644 index 00000000000000..394e23cbbf646c --- /dev/null +++ b/x-pack/plugins/security/public/plugin.tsx @@ -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 { Plugin, CoreSetup, CoreStart } from 'src/core/public'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; +import { + FeatureCatalogueCategory, + HomePublicPluginSetup, +} from '../../../../src/plugins/home/public'; +import { LicensingPluginSetup } from '../../licensing/public'; +import { ManagementSetup, ManagementStart } from '../../../../src/plugins/management/public'; +import { + SessionExpired, + SessionTimeout, + ISessionTimeout, + SessionTimeoutHttpInterceptor, + UnauthorizedResponseHttpInterceptor, +} from './session'; +import { SecurityLicenseService } from '../common/licensing'; +import { SecurityNavControlService } from './nav_control'; +import { AccountManagementPage } from './account_management'; +import { AuthenticationService, AuthenticationServiceSetup } from './authentication'; +import { ManagementService, UserAPIClient } from './management'; + +export interface PluginSetupDependencies { + licensing: LicensingPluginSetup; + home?: HomePublicPluginSetup; + management?: ManagementSetup; +} + +export interface PluginStartDependencies { + data: DataPublicPluginStart; + management?: ManagementStart; +} + +export class SecurityPlugin + implements + Plugin< + SecurityPluginSetup, + SecurityPluginStart, + PluginSetupDependencies, + PluginStartDependencies + > { + private sessionTimeout!: ISessionTimeout; + private readonly navControlService = new SecurityNavControlService(); + private readonly securityLicenseService = new SecurityLicenseService(); + private readonly managementService = new ManagementService(); + private authc!: AuthenticationServiceSetup; + + public setup( + core: CoreSetup, + { home, licensing, management }: PluginSetupDependencies + ) { + const { http, notifications, injectedMetadata } = core; + const { basePath, anonymousPaths } = http; + anonymousPaths.register('/login'); + anonymousPaths.register('/logout'); + anonymousPaths.register('/logged_out'); + + const tenant = `${injectedMetadata.getInjectedVar('session.tenant', '')}`; + const sessionExpired = new SessionExpired(basePath, tenant); + http.intercept(new UnauthorizedResponseHttpInterceptor(sessionExpired, anonymousPaths)); + this.sessionTimeout = new SessionTimeout(notifications, sessionExpired, http, tenant); + http.intercept(new SessionTimeoutHttpInterceptor(this.sessionTimeout, anonymousPaths)); + + const { license } = this.securityLicenseService.setup({ license$: licensing.license$ }); + + this.authc = new AuthenticationService().setup({ http: core.http }); + + this.navControlService.setup({ + securityLicense: license, + authc: this.authc, + }); + + if (management) { + this.managementService.setup({ + license, + management, + authc: this.authc, + fatalErrors: core.fatalErrors, + getStartServices: core.getStartServices, + }); + } + + if (management && home) { + home.featureCatalogue.register({ + id: 'security', + title: i18n.translate('xpack.security.registerFeature.securitySettingsTitle', { + defaultMessage: 'Security Settings', + }), + description: i18n.translate('xpack.security.registerFeature.securitySettingsDescription', { + defaultMessage: + 'Protect your data and easily manage who has access to what with users and roles.', + }), + icon: 'securityApp', + path: '/app/kibana#/management/security/users', + showOnHomePage: true, + category: FeatureCatalogueCategory.ADMIN, + }); + } + + return { + authc: this.authc, + sessionTimeout: this.sessionTimeout, + }; + } + + public start(core: CoreStart, { data, management }: PluginStartDependencies) { + this.sessionTimeout.start(); + this.navControlService.start({ core }); + + if (management) { + this.managementService.start({ management }); + } + + return { + __legacyCompat: { + account_management: { + AccountManagementPage: () => ( + + + + ), + }, + }, + }; + } + + public stop() { + this.sessionTimeout.stop(); + this.navControlService.stop(); + this.securityLicenseService.stop(); + this.managementService.stop(); + } +} + +export type SecurityPluginSetup = ReturnType; +export type SecurityPluginStart = ReturnType; diff --git a/x-pack/plugins/security/server/routes/role_mapping/get.ts b/x-pack/plugins/security/server/routes/role_mapping/get.ts index 9cd5cf83092e18..def6fabc0e3222 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/get.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/get.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { schema } from '@kbn/config-schema'; -import { RoleMapping } from '../../../../../legacy/plugins/security/common/model'; +import { RoleMapping } from '../../../common/model'; import { createLicensedRouteHandler } from '../licensed_route_handler'; import { wrapError } from '../../errors'; import { RouteDefinitionParams } from '..'; diff --git a/x-pack/plugins/spaces/common/index.ts b/x-pack/plugins/spaces/common/index.ts index 65baa1bd991022..c1f0f8bd3ece41 100644 --- a/x-pack/plugins/spaces/common/index.ts +++ b/x-pack/plugins/spaces/common/index.ts @@ -5,5 +5,5 @@ */ export { isReservedSpace } from './is_reserved_space'; -export { MAX_SPACE_INITIALS } from './constants'; +export { MAX_SPACE_INITIALS, SPACE_SEARCH_COUNT_THRESHOLD } from './constants'; export { addSpaceIdToPath, getSpaceIdFromPath } from './lib/spaces_url_parser'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index ce3edbbb598280..c3e46a9c04e1cc 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -10595,17 +10595,6 @@ "xpack.security.management.apiKeys.table.userFilterLabel": "ユーザー", "xpack.security.management.apiKeys.table.userNameColumnName": "ユーザー", "xpack.security.management.apiKeysTitle": "API キー", - "xpack.security.management.changePasswordForm.cancelButtonLabel": "キャンセル", - "xpack.security.management.changePasswordForm.changePasswordLinkLabel": "パスワードを変更", - "xpack.security.management.changePasswordForm.confirmPasswordLabel": "パスワードの確認", - "xpack.security.management.changePasswordForm.currentPasswordLabel": "現在のパスワード", - "xpack.security.management.changePasswordForm.incorrectPasswordDescription": "入力された現在のパスワードが正しくありません。", - "xpack.security.management.changePasswordForm.newPasswordLabel": "新しいパスワード", - "xpack.security.management.changePasswordForm.passwordDontMatchDescription": "パスワードが一致しません", - "xpack.security.management.changePasswordForm.passwordLabel": "パスワード", - "xpack.security.management.changePasswordForm.passwordLengthDescription": "パスワードは最低 6 文字必要です", - "xpack.security.management.changePasswordForm.saveChangesButtonLabel": "変更を保存", - "xpack.security.management.changePasswordForm.updateAndRestartKibanaDescription": "Kibana ユーザーのパスワードを変更後、kibana.yml ファイルを更新し Kibana を再起動する必要があります。", "xpack.security.management.editRole.cancelButtonLabel": "キャンセル", "xpack.security.management.editRole.changeAllPrivilegesLink": "(すべて変更)", "xpack.security.management.editRole.collapsiblePanel.hideLinkText": "非表示", @@ -10730,10 +10719,6 @@ "xpack.security.management.editRolespacePrivilegeForm.createPrivilegeButton": "スペース権限を作成", "xpack.security.management.editRolespacePrivilegeForm.updateGlobalPrivilegeButton": "グローバル特権を更新", "xpack.security.management.editRolespacePrivilegeForm.updatePrivilegeButton": "スペース権限を更新", - "xpack.security.management.passwordForm.confirmPasswordLabel": "パスワードの確認", - "xpack.security.management.passwordForm.passwordDontMatchDescription": "パスワードが一致しません", - "xpack.security.management.passwordForm.passwordLabel": "パスワード", - "xpack.security.management.passwordForm.passwordLengthDescription": "パスワードは最低 6 文字必要です", "xpack.security.management.roles.actionsColumnName": "アクション", "xpack.security.management.roles.cloneRoleActionName": "{roleName} を複製", "xpack.security.management.roles.confirmDelete.cancelButtonLabel": "キャンセル", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index dc72e83ebe3c30..c40b8c8d0393c8 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10594,17 +10594,6 @@ "xpack.security.management.apiKeys.table.userFilterLabel": "用户", "xpack.security.management.apiKeys.table.userNameColumnName": "用户", "xpack.security.management.apiKeysTitle": "API 密钥", - "xpack.security.management.changePasswordForm.cancelButtonLabel": "取消", - "xpack.security.management.changePasswordForm.changePasswordLinkLabel": "更改密码", - "xpack.security.management.changePasswordForm.confirmPasswordLabel": "确认密码", - "xpack.security.management.changePasswordForm.currentPasswordLabel": "当前密码", - "xpack.security.management.changePasswordForm.incorrectPasswordDescription": "您输入的当前密码不正确。", - "xpack.security.management.changePasswordForm.newPasswordLabel": "新密码", - "xpack.security.management.changePasswordForm.passwordDontMatchDescription": "密码不匹配", - "xpack.security.management.changePasswordForm.passwordLabel": "密码", - "xpack.security.management.changePasswordForm.passwordLengthDescription": "密码长度必须至少为 6 个字符", - "xpack.security.management.changePasswordForm.saveChangesButtonLabel": "保存更改", - "xpack.security.management.changePasswordForm.updateAndRestartKibanaDescription": "更改 Kibana 用户的密码后,必须更新 kibana.yml 文件并重新启动 Kibana", "xpack.security.management.editRole.cancelButtonLabel": "取消", "xpack.security.management.editRole.changeAllPrivilegesLink": "(全部更改)", "xpack.security.management.editRole.collapsiblePanel.hideLinkText": "隐藏", @@ -10729,10 +10718,6 @@ "xpack.security.management.editRolespacePrivilegeForm.createPrivilegeButton": "创建工作区权限", "xpack.security.management.editRolespacePrivilegeForm.updateGlobalPrivilegeButton": "更新全局权限", "xpack.security.management.editRolespacePrivilegeForm.updatePrivilegeButton": "更新工作区权限", - "xpack.security.management.passwordForm.confirmPasswordLabel": "确认密码", - "xpack.security.management.passwordForm.passwordDontMatchDescription": "密码不匹配", - "xpack.security.management.passwordForm.passwordLabel": "密码", - "xpack.security.management.passwordForm.passwordLengthDescription": "密码长度必须至少为 6 个字符", "xpack.security.management.roles.actionsColumnName": "鎿嶄綔", "xpack.security.management.roles.cloneRoleActionName": "克隆 {roleName}", "xpack.security.management.roles.confirmDelete.cancelButtonLabel": "取消", diff --git a/x-pack/test/functional/apps/security/management.js b/x-pack/test/functional/apps/security/management.js index 45a35029ffba2e..8ab84126b2b30a 100644 --- a/x-pack/test/functional/apps/security/management.js +++ b/x-pack/test/functional/apps/security/management.js @@ -11,7 +11,7 @@ import { ROLES_PATH, EDIT_ROLES_PATH, CLONE_ROLES_PATH, -} from '../../../../legacy/plugins/security/public/views/management/management_urls'; +} from '../../../../plugins/security/public/management/management_urls'; export default function({ getService, getPageObjects }) { const kibanaServer = getService('kibanaServer'); diff --git a/x-pack/test/functional/apps/security/role_mappings.ts b/x-pack/test/functional/apps/security/role_mappings.ts index 5fed56ee79e3dd..a1517e1934a286 100644 --- a/x-pack/test/functional/apps/security/role_mappings.ts +++ b/x-pack/test/functional/apps/security/role_mappings.ts @@ -93,7 +93,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const url = parse(await browser.getCurrentUrl()); - expect(url.hash).to.eql('#/management/security/role_mappings?_g=()'); + expect(url.hash).to.eql('#/management/security/role_mappings'); }); describe('with role mappings', () => {