From 5788a6bd785d4848c95ec94a79fe3ea87c4085f0 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Tue, 2 Mar 2021 11:22:20 -0600 Subject: [PATCH] [Workplace Search] Role Mappings to Kibana (#93123) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add routes for role mapings * Initial copy/paste * Update RoleMappingsRouter - Update all paths - Change router to use children instead of render props - Remove legacy app chrome * Update RoleMappings - Update all paths - Use global flash messages * Update RoleMapping - Update all paths - Use global flash messages - Add types to fix errors - Use React Router Hooks instead of legacy withRouter HOC * Fix path in index and add route helper * Update paths in RoleMappingsLogic * Remove history in favor of KibanaLogic.navigateToUrl * Add Role type * Remove ID prop This is not needed because the ID is actually passed in the URL itself and is not a requirement in the body of the request * Replace contextual flash messages with global It appeared that the server sometimes sent flash messages with the API response, but I checked the Rails server code and there is no `flashMessages` sent back from the server so I am omitting that from the `RoleMappingsServerDetails` interface as well. * Replace Rails http with kibana http * Fix route path * Add route and update global navigation * Add breadcrumb/page title * Update flash messages in RoleMapping I did this for RoleMappings but forgot this one * Use explicit AttributeName type instead of string * Add i18n * Fix type issue Because the shared role mapping components work for both App Search and Workplace Search, the more generic string is used here because App Search has different role names. * Add tests for components and router * Add optional to interface In the case of a new role mapping, the server is called at the ‘/new’ route and the server responds without a roleMapping prop, as it has not yet been created. * Add tests for RoleMappingsLogic Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/layout/nav.tsx | 5 +- .../applications/workplace_search/index.tsx | 7 + .../applications/workplace_search/routes.ts | 1 + .../applications/workplace_search/types.ts | 2 + .../views/role_mappings/constants.ts | 105 +++++ .../views/role_mappings/index.ts | 8 + .../views/role_mappings/role_mapping.test.tsx | 121 ++++++ .../views/role_mappings/role_mapping.tsx | 216 ++++++++++ .../role_mappings/role_mappings.test.tsx | 57 +++ .../views/role_mappings/role_mappings.tsx | 70 ++++ .../role_mappings/role_mappings_logic.test.ts | 394 ++++++++++++++++++ .../role_mappings/role_mappings_logic.ts | 311 ++++++++++++++ .../role_mappings_router.test.tsx | 26 ++ .../role_mappings/role_mappings_router.tsx | 34 ++ .../server/routes/workplace_search/index.ts | 2 + .../workplace_search/role_mappings.test.ts | 154 +++++++ .../routes/workplace_search/role_mappings.ts | 117 ++++++ 17 files changed, 1626 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/constants.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_router.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_router.tsx create mode 100644 x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts create mode 100644 x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx index 16722c1554ddf..f2edc04a5661c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx @@ -10,7 +10,6 @@ import React from 'react'; import { EuiSpacer } from '@elastic/eui'; import { WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants'; -import { getWorkplaceSearchUrl } from '../../../shared/enterprise_search_url'; import { SideNav, SideNavLink } from '../../../shared/layout'; import { NAV } from '../../constants'; import { @@ -43,9 +42,7 @@ export const WorkplaceSearchNav: React.FC = ({ {NAV.GROUPS} - - {NAV.ROLE_MAPPINGS} - + {NAV.ROLE_MAPPINGS} {NAV.SECURITY} {NAV.SETTINGS} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index 3cd1a3bd136b8..656c93053e22b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -26,6 +26,7 @@ import { SOURCES_PATH, PERSONAL_SOURCES_PATH, ORG_SETTINGS_PATH, + ROLE_MAPPINGS_PATH, SECURITY_PATH, } from './routes'; import { SourcesRouter } from './views/content_sources'; @@ -36,6 +37,7 @@ import { GroupsRouter } from './views/groups'; import { GroupSubNav } from './views/groups/components/group_sub_nav'; import { Overview } from './views/overview'; import { Overview as OverviewMVP } from './views/overview_mvp'; +import { RoleMappingsRouter } from './views/role_mappings'; import { Security } from './views/security'; import { SettingsRouter } from './views/settings'; import { SettingsSubNav } from './views/settings/components/settings_sub_nav'; @@ -111,6 +113,11 @@ export const WorkplaceSearchConfigured: React.FC = (props) => { + + } restrictWidth readOnlyMode={readOnlyMode}> + + + } restrictWidth readOnlyMode={readOnlyMode}> diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts index 462f89abd6143..50f6596a860c5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts @@ -133,3 +133,4 @@ export const getReindexJobRoute = ( isOrganization: boolean ) => getSourcesPath(generatePath(REINDEX_JOB_PATH, { sourceId, activeReindexJobId }), isOrganization); +export const getRoleMappingPath = (roleId: string) => generatePath(ROLE_MAPPING_PATH, { roleId }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts index 44e0fd5b6f287..79fe6dc8c92cb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts @@ -22,6 +22,8 @@ export interface Meta { page: MetaPage; } +export type Role = 'admin' | 'user'; + export interface Group { id: string; name: string; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/constants.ts new file mode 100644 index 0000000000000..5930f7862cd83 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/constants.ts @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const DELETE_ROLE_MAPPING_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.roleMapping.deleteRoleMappingButtonMessage', + { + defaultMessage: + 'Are you sure you want to permanently delete this mapping? This action is not reversible and some users might lose access.', + } +); + +export const DEFAULT_GROUP_NAME = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.roleMapping.defaultGroupName', + { + defaultMessage: 'Default', + } +); + +export const ADMIN_ROLE_TYPE_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.roleMapping.adminRoleTypeDescription', + { + defaultMessage: + 'Admins have complete access to all organization-wide settings, including content source, group and user management functionality.', + } +); + +export const USER_ROLE_TYPE_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.roleMapping.userRoleTypeDescription', + { + defaultMessage: + "Users' feature access is limited to search interfaces and personal settings management.", + } +); + +export const ROLE_SELECTOR_DISABLED_TEXT = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.roleMapping.roleSelectorDisabledText', + { + defaultMessage: + 'You need at least one admin role mapping before you can create a user role mapping.', + } +); + +export const GROUP_ASSIGNMENT_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.roleMapping.groupAssignmentTitle', + { + defaultMessage: 'Group assignment', + } +); + +export const GROUP_ASSIGNMENT_INVALID_ERROR = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.roleMapping.groupAssignmentInvalidError', + { + defaultMessage: 'At least one assigned group is required.', + } +); + +export const GROUP_ASSIGNMENT_ALL_GROUPS_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.roleMapping.groupAssignmentAllGroupsLabel', + { + defaultMessage: 'Include in all groups, including future groups', + } +); + +export const EMPTY_ROLE_MAPPINGS_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.roleMapping.emptyRoleMappingsTitle', + { + defaultMessage: 'No role mappings yet', + } +); + +export const EMPTY_ROLE_MAPPINGS_BODY = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.roleMapping.emptyRoleMappingsBody', + { + defaultMessage: + 'New team members are assigned the admin role by default. An admin can access everything. Create a new role to override the default.', + } +); + +export const ROLE_MAPPINGS_TABLE_HEADER = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.roleMapping.roleMappingsTableHeader', + { + defaultMessage: 'Group Access', + } +); + +export const ROLE_MAPPINGS_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.roleMapping.roleMappingsTitle', + { + defaultMessage: 'Users & roles', + } +); + +export const ROLE_MAPPINGS_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.roleMapping.roleMappingsDescription', + { + defaultMessage: + 'Define role mappings for elasticsearch-native and elasticsearch-saml authentication.', + } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/index.ts new file mode 100644 index 0000000000000..ce4b1de6e399d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { RoleMappingsRouter } from './role_mappings_router'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.test.tsx new file mode 100644 index 0000000000000..4742b741c9640 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.test.tsx @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import '../../../__mocks__/shallow_useeffect.mock'; +import { setMockActions, setMockValues } from '../../../__mocks__'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiCheckbox } from '@elastic/eui'; + +import { Loading } from '../../../shared/loading'; +import { + AttributeSelector, + DeleteMappingCallout, + RoleSelector, +} from '../../../shared/role_mapping'; +import { wsRoleMapping } from '../../../shared/role_mapping/__mocks__/roles'; + +import { RoleMapping } from './role_mapping'; + +describe('RoleMapping', () => { + const initializeRoleMappings = jest.fn(); + const initializeRoleMapping = jest.fn(); + const handleSaveMapping = jest.fn(); + const handleGroupSelectionChange = jest.fn(); + const handleAllGroupsSelectionChange = jest.fn(); + const handleAttributeValueChange = jest.fn(); + const handleAttributeSelectorChange = jest.fn(); + const handleDeleteMapping = jest.fn(); + const handleRoleChange = jest.fn(); + const handleAuthProviderChange = jest.fn(); + const resetState = jest.fn(); + const groups = [ + { + name: 'Group 1', + id: 'g1', + }, + { + name: 'Group 2', + id: 'g2', + }, + ]; + const mockValues = { + attributes: [], + elasticsearchRoles: [], + dataLoading: false, + roleType: 'admin', + roleMappings: [wsRoleMapping], + attributeValue: '', + attributeName: 'username', + availableGroups: groups, + selectedGroups: new Set(), + includeInAllGroups: false, + availableAuthProviders: [], + multipleAuthProvidersConfig: true, + selectedAuthProviders: [], + }; + + beforeEach(() => { + setMockActions({ + initializeRoleMappings, + initializeRoleMapping, + handleSaveMapping, + handleGroupSelectionChange, + handleAllGroupsSelectionChange, + handleAttributeValueChange, + handleAttributeSelectorChange, + handleDeleteMapping, + handleRoleChange, + handleAuthProviderChange, + resetState, + }); + setMockValues(mockValues); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(AttributeSelector)).toHaveLength(1); + expect(wrapper.find(RoleSelector)).toHaveLength(2); + }); + + it('returns Loading when loading', () => { + setMockValues({ ...mockValues, dataLoading: true }); + const wrapper = shallow(); + + expect(wrapper.find(Loading)).toHaveLength(1); + }); + + it('hides DeleteMappingCallout for new mapping', () => { + const wrapper = shallow(); + + expect(wrapper.find(DeleteMappingCallout)).toHaveLength(0); + }); + + it('handles group checkbox click', () => { + const wrapper = shallow(); + wrapper + .find(EuiCheckbox) + .first() + .simulate('change', { target: { checked: true } }); + + expect(handleGroupSelectionChange).toHaveBeenCalledWith(groups[0].id, true); + }); + + it('handles all groups checkbox click', () => { + const wrapper = shallow(); + wrapper + .find(EuiCheckbox) + .last() + .simulate('change', { target: { checked: true } }); + + expect(handleAllGroupsSelectionChange).toHaveBeenCalledWith(true); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.tsx new file mode 100644 index 0000000000000..b2911bbcc64c2 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mapping.tsx @@ -0,0 +1,216 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect } from 'react'; + +import { useParams } from 'react-router-dom'; + +import { useActions, useValues } from 'kea'; + +import { + EuiButton, + EuiCheckbox, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiPanel, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { FlashMessages } from '../../../shared/flash_messages'; +import { Loading } from '../../../shared/loading'; +import { + AttributeSelector, + DeleteMappingCallout, + RoleSelector, +} from '../../../shared/role_mapping'; +import { ROLE_LABEL } from '../../../shared/role_mapping/constants'; +import { ViewContentHeader } from '../../components/shared/view_content_header'; +import { Role } from '../../types'; + +import { + ADMIN_ROLE_TYPE_DESCRIPTION, + USER_ROLE_TYPE_DESCRIPTION, + ROLE_SELECTOR_DISABLED_TEXT, + GROUP_ASSIGNMENT_TITLE, + GROUP_ASSIGNMENT_INVALID_ERROR, + GROUP_ASSIGNMENT_ALL_GROUPS_LABEL, +} from './constants'; + +import { RoleMappingsLogic } from './role_mappings_logic'; + +interface RoleType { + type: Role; + description: string; +} + +const roleTypes = [ + { + type: 'admin', + description: ADMIN_ROLE_TYPE_DESCRIPTION, + }, + { + type: 'user', + description: USER_ROLE_TYPE_DESCRIPTION, + }, +] as RoleType[]; + +interface RoleMappingProps { + isNew?: boolean; +} + +export const RoleMapping: React.FC = ({ isNew }) => { + const { roleId } = useParams() as { roleId: string }; + const { + initializeRoleMappings, + initializeRoleMapping, + handleSaveMapping, + handleGroupSelectionChange, + handleAllGroupsSelectionChange, + handleAttributeValueChange, + handleAttributeSelectorChange, + handleDeleteMapping, + handleRoleChange, + handleAuthProviderChange, + resetState, + } = useActions(RoleMappingsLogic); + + const { + attributes, + elasticsearchRoles, + dataLoading, + roleType, + roleMappings, + attributeValue, + attributeName, + availableGroups, + selectedGroups, + includeInAllGroups, + availableAuthProviders, + multipleAuthProvidersConfig, + selectedAuthProviders, + } = useValues(RoleMappingsLogic); + + useEffect(() => { + initializeRoleMappings(); + initializeRoleMapping(roleId); + return resetState; + }, [roleId]); + + if (dataLoading) return ; + + const hasGroupAssignment = selectedGroups.size > 0 || includeInAllGroups; + + const SAVE_ROLE_MAPPING_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.roleMapping.saveRoleMappingButtonMessage', + { + defaultMessage: '{operation} role mapping', + values: { operation: isNew ? 'Save' : 'Update' }, + } + ); + + const saveRoleMappingButton = ( + + {SAVE_ROLE_MAPPING_LABEL} + + ); + + const hasAdminRoleMapping = roleMappings.some( + ({ roleType: roleMappingRoleType }: { roleType: string }) => + roleMappingRoleType === ('admin' as string) + ); + + return ( + <> + + +
+ + + + + + + +

{ROLE_LABEL}

+
+ + {roleTypes.map(({ type, description }) => ( + + ))} +
+
+ + + +

{GROUP_ASSIGNMENT_TITLE}

+
+ +
+ + <> + {availableGroups.map(({ id, name }) => ( + { + handleGroupSelectionChange(id, e.target.checked); + }} + label={name} + disabled={includeInAllGroups} + /> + ))} + + { + handleAllGroupsSelectionChange(e.target.checked); + }} + label={GROUP_ASSIGNMENT_ALL_GROUPS_LABEL} + /> + + +
+
+
+
+ + {!isNew && } +
+ + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.test.tsx new file mode 100644 index 0000000000000..c6da903e20912 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.test.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import '../../../__mocks__/shallow_useeffect.mock'; +import { setMockActions, setMockValues } from '../../../__mocks__'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiEmptyPrompt } from '@elastic/eui'; + +import { Loading } from '../../../shared/loading'; +import { RoleMappingsTable } from '../../../shared/role_mapping'; +import { wsRoleMapping } from '../../../shared/role_mapping/__mocks__/roles'; + +import { RoleMappings } from './role_mappings'; + +describe('RoleMappings', () => { + const initializeRoleMappings = jest.fn(); + const mockValues = { + roleMappings: [wsRoleMapping], + dataLoading: false, + multipleAuthProvidersConfig: false, + }; + + beforeEach(() => { + setMockActions({ + initializeRoleMappings, + }); + setMockValues(mockValues); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(RoleMappingsTable)).toHaveLength(1); + }); + + it('returns Loading when loading', () => { + setMockValues({ ...mockValues, dataLoading: true }); + const wrapper = shallow(); + + expect(wrapper.find(Loading)).toHaveLength(1); + }); + + it('renders empty state', () => { + setMockValues({ ...mockValues, roleMappings: [] }); + const wrapper = shallow(); + + expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx new file mode 100644 index 0000000000000..e47b2646459df --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect } from 'react'; + +import { useActions, useValues } from 'kea'; + +import { EuiEmptyPrompt } from '@elastic/eui'; + +import { FlashMessages } from '../../../shared/flash_messages'; +import { Loading } from '../../../shared/loading'; +import { AddRoleMappingButton, RoleMappingsTable } from '../../../shared/role_mapping'; +import { ViewContentHeader } from '../../components/shared/view_content_header'; +import { getRoleMappingPath, ROLE_MAPPING_NEW_PATH } from '../../routes'; + +import { + EMPTY_ROLE_MAPPINGS_TITLE, + EMPTY_ROLE_MAPPINGS_BODY, + ROLE_MAPPINGS_TABLE_HEADER, + ROLE_MAPPINGS_TITLE, + ROLE_MAPPINGS_DESCRIPTION, +} from './constants'; + +import { RoleMappingsLogic } from './role_mappings_logic'; + +export const RoleMappings: React.FC = () => { + const { initializeRoleMappings } = useActions(RoleMappingsLogic); + + const { roleMappings, dataLoading, multipleAuthProvidersConfig } = useValues(RoleMappingsLogic); + + useEffect(() => { + initializeRoleMappings(); + }, []); + + if (dataLoading) return ; + + const addMappingButton = ; + const emptyPrompt = ( + {EMPTY_ROLE_MAPPINGS_TITLE}} + body={

{EMPTY_ROLE_MAPPINGS_BODY}

} + actions={addMappingButton} + /> + ); + const roleMappingsTable = ( + + ); + + return ( + <> + +
+ + {roleMappings.length === 0 ? emptyPrompt : roleMappingsTable} +
+ + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts new file mode 100644 index 0000000000000..a9526d9450993 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts @@ -0,0 +1,394 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mockFlashMessageHelpers, mockHttpValues, mockKibanaValues } from '../../../__mocks__'; +import { LogicMounter } from '../../../__mocks__/kea.mock'; + +import { groups } from '../../__mocks__/groups.mock'; + +import { nextTick } from '@kbn/test/jest'; + +import { wsRoleMapping } from '../../../shared/role_mapping/__mocks__/roles'; +import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants'; + +import { RoleMappingsLogic } from './role_mappings_logic'; + +describe('RoleMappingsLogic', () => { + const { http } = mockHttpValues; + const { navigateToUrl } = mockKibanaValues; + const { clearFlashMessages, flashAPIErrors } = mockFlashMessageHelpers; + const { mount } = new LogicMounter(RoleMappingsLogic); + const defaultValues = { + attributes: [], + availableAuthProviders: [], + elasticsearchRoles: [], + roleMapping: null, + roleMappings: [], + roleType: 'admin', + attributeValue: '', + attributeName: 'username', + dataLoading: true, + multipleAuthProvidersConfig: false, + availableGroups: [], + selectedGroups: new Set(), + includeInAllGroups: false, + selectedAuthProviders: [ANY_AUTH_PROVIDER], + }; + const roleGroup = { + id: '123', + name: 'Role Group', + }; + const defaultGroup = { + id: '124', + name: 'Default', + }; + + const mappingsServerProps = { multipleAuthProvidersConfig: true, roleMappings: [wsRoleMapping] }; + const mappingServerProps = { + attributes: [], + authProviders: [], + availableGroups: [roleGroup, defaultGroup], + elasticsearchRoles: [], + multipleAuthProvidersConfig: false, + roleMapping: wsRoleMapping, + }; + + beforeEach(() => { + jest.clearAllMocks(); + mount(); + }); + + it('has expected default values', () => { + expect(RoleMappingsLogic.values).toEqual(defaultValues); + }); + + describe('actions', () => { + it('setRoleMappingsData', () => { + RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps); + + expect(RoleMappingsLogic.values.roleMappings).toEqual([wsRoleMapping]); + expect(RoleMappingsLogic.values.dataLoading).toEqual(false); + expect(RoleMappingsLogic.values.multipleAuthProvidersConfig).toEqual(true); + }); + + describe('setRoleMappingData', () => { + it('sets data correctly', () => { + RoleMappingsLogic.actions.setRoleMappingData(mappingServerProps); + + expect(RoleMappingsLogic.values.roleMapping).toEqual(wsRoleMapping); + expect(RoleMappingsLogic.values.dataLoading).toEqual(false); + expect(RoleMappingsLogic.values.attributes).toEqual(mappingServerProps.attributes); + expect(RoleMappingsLogic.values.availableGroups).toEqual( + mappingServerProps.availableGroups + ); + expect(RoleMappingsLogic.values.includeInAllGroups).toEqual(true); + expect(RoleMappingsLogic.values.elasticsearchRoles).toEqual( + mappingServerProps.elasticsearchRoles + ); + expect(RoleMappingsLogic.values.selectedGroups).toEqual( + new Set([wsRoleMapping.groups[0].id]) + ); + }); + + it('sets default group with new role mapping', () => { + RoleMappingsLogic.actions.setRoleMappingData({ + ...mappingServerProps, + roleMapping: undefined, + }); + + expect(RoleMappingsLogic.values.selectedGroups).toEqual(new Set([defaultGroup.id])); + }); + }); + + it('handleRoleChange', () => { + RoleMappingsLogic.actions.handleRoleChange('user'); + + expect(RoleMappingsLogic.values.roleType).toEqual('user'); + }); + + it('handleGroupSelectionChange', () => { + const group = wsRoleMapping.groups[0]; + const otherGroup = groups[0]; + RoleMappingsLogic.actions.setRoleMappingData({ + ...mappingServerProps, + roleMapping: { + ...wsRoleMapping, + groups: [group, otherGroup], + }, + }); + + RoleMappingsLogic.actions.handleGroupSelectionChange(otherGroup.id, true); + expect(RoleMappingsLogic.values.selectedGroups).toEqual(new Set([group.id, otherGroup.id])); + + RoleMappingsLogic.actions.handleGroupSelectionChange(otherGroup.id, false); + expect(RoleMappingsLogic.values.selectedGroups).toEqual(new Set([group.id])); + }); + + it('handleAllGroupsSelectionChange', () => { + RoleMappingsLogic.actions.handleAllGroupsSelectionChange(true); + + expect(RoleMappingsLogic.values.includeInAllGroups).toEqual(true); + }); + + describe('handleAttributeSelectorChange', () => { + const elasticsearchRoles = ['foo', 'bar']; + + it('sets values correctly', () => { + RoleMappingsLogic.actions.setRoleMappingData({ + ...mappingServerProps, + elasticsearchRoles, + }); + RoleMappingsLogic.actions.handleAttributeSelectorChange('role', elasticsearchRoles[0]); + + expect(RoleMappingsLogic.values.attributeValue).toEqual(elasticsearchRoles[0]); + expect(RoleMappingsLogic.values.attributeName).toEqual('role'); + }); + + it('correctly handles "role" fallback', () => { + RoleMappingsLogic.actions.handleAttributeSelectorChange('username', elasticsearchRoles[0]); + + expect(RoleMappingsLogic.values.attributeValue).toEqual(''); + }); + }); + + it('handleAttributeValueChange', () => { + RoleMappingsLogic.actions.handleAttributeValueChange('changed_value'); + + expect(RoleMappingsLogic.values.attributeValue).toEqual('changed_value'); + }); + + describe('handleAuthProviderChange', () => { + beforeEach(() => { + RoleMappingsLogic.actions.setRoleMappingData({ + ...mappingServerProps, + roleMapping: { + ...wsRoleMapping, + authProvider: ['foo'], + }, + }); + }); + const providers = ['bar', 'baz']; + const providerWithAny = [ANY_AUTH_PROVIDER, providers[1]]; + it('handles empty state', () => { + RoleMappingsLogic.actions.handleAuthProviderChange([]); + + expect(RoleMappingsLogic.values.selectedAuthProviders).toEqual([ANY_AUTH_PROVIDER]); + }); + + it('handles single value', () => { + RoleMappingsLogic.actions.handleAuthProviderChange([providers[0]]); + + expect(RoleMappingsLogic.values.selectedAuthProviders).toEqual([providers[0]]); + }); + + it('handles multiple values', () => { + RoleMappingsLogic.actions.handleAuthProviderChange(providers); + + expect(RoleMappingsLogic.values.selectedAuthProviders).toEqual(providers); + }); + + it('handles "any" auth in previous state', () => { + RoleMappingsLogic.actions.setRoleMappingData({ + ...mappingServerProps, + roleMapping: { + ...wsRoleMapping, + authProvider: [ANY_AUTH_PROVIDER], + }, + }); + RoleMappingsLogic.actions.handleAuthProviderChange(providerWithAny); + + expect(RoleMappingsLogic.values.selectedAuthProviders).toEqual([providers[1]]); + }); + + it('handles catch-all state', () => { + RoleMappingsLogic.actions.handleAuthProviderChange(providerWithAny); + + expect(RoleMappingsLogic.values.selectedAuthProviders).toEqual([ANY_AUTH_PROVIDER]); + }); + }); + + it('resetState', () => { + RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps); + RoleMappingsLogic.actions.setRoleMappingData(mappingServerProps); + RoleMappingsLogic.actions.resetState(); + + expect(RoleMappingsLogic.values.dataLoading).toEqual(true); + expect(RoleMappingsLogic.values.roleMappings).toEqual([]); + expect(RoleMappingsLogic.values.roleMapping).toEqual(null); + expect(RoleMappingsLogic.values.attributeValue).toEqual(''); + expect(RoleMappingsLogic.values.attributeName).toEqual('username'); + expect(clearFlashMessages).toHaveBeenCalled(); + }); + }); + + describe('listeners', () => { + describe('initializeRoleMappings', () => { + it('calls API and sets values', async () => { + const setRoleMappingsDataSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMappingsData'); + http.get.mockReturnValue(Promise.resolve(mappingsServerProps)); + RoleMappingsLogic.actions.initializeRoleMappings(); + + expect(http.get).toHaveBeenCalledWith('/api/workplace_search/org/role_mappings'); + await nextTick(); + expect(setRoleMappingsDataSpy).toHaveBeenCalledWith(mappingsServerProps); + }); + + it('handles error', async () => { + http.get.mockReturnValue(Promise.reject('this is an error')); + RoleMappingsLogic.actions.initializeRoleMappings(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + }); + }); + + describe('initializeRoleMapping', () => { + it('calls API and sets values for new mapping', async () => { + const setRoleMappingDataSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMappingData'); + http.get.mockReturnValue(Promise.resolve(mappingServerProps)); + RoleMappingsLogic.actions.initializeRoleMapping(); + + expect(http.get).toHaveBeenCalledWith('/api/workplace_search/org/role_mappings/new'); + await nextTick(); + expect(setRoleMappingDataSpy).toHaveBeenCalledWith(mappingServerProps); + }); + + it('calls API and sets values for existing mapping', async () => { + const setRoleMappingDataSpy = jest.spyOn(RoleMappingsLogic.actions, 'setRoleMappingData'); + http.get.mockReturnValue(Promise.resolve(mappingServerProps)); + RoleMappingsLogic.actions.initializeRoleMapping('123'); + + expect(http.get).toHaveBeenCalledWith('/api/workplace_search/org/role_mappings/123'); + await nextTick(); + expect(setRoleMappingDataSpy).toHaveBeenCalledWith(mappingServerProps); + }); + + it('handles error', async () => { + http.get.mockReturnValue(Promise.reject('this is an error')); + RoleMappingsLogic.actions.initializeRoleMapping(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + }); + + it('redirects when there is a 404 status', async () => { + http.get.mockReturnValue(Promise.reject({ status: 404 })); + RoleMappingsLogic.actions.initializeRoleMapping(); + await nextTick(); + + expect(navigateToUrl).toHaveBeenCalled(); + }); + }); + + describe('handleSaveMapping', () => { + it('calls API and navigates when new mapping', async () => { + RoleMappingsLogic.actions.setRoleMappingsData(mappingsServerProps); + + http.post.mockReturnValue(Promise.resolve(mappingServerProps)); + RoleMappingsLogic.actions.handleSaveMapping(); + + expect(http.post).toHaveBeenCalledWith('/api/workplace_search/org/role_mappings', { + body: JSON.stringify({ + rules: { + username: '', + }, + roleType: 'admin', + groups: [], + allGroups: false, + authProvider: [ANY_AUTH_PROVIDER], + }), + }); + await nextTick(); + + expect(navigateToUrl).toHaveBeenCalled(); + }); + + it('calls API and navigates when existing mapping', async () => { + RoleMappingsLogic.actions.setRoleMappingData(mappingServerProps); + + http.put.mockReturnValue(Promise.resolve(mappingServerProps)); + RoleMappingsLogic.actions.handleSaveMapping(); + + expect(http.put).toHaveBeenCalledWith( + `/api/workplace_search/org/role_mappings/${wsRoleMapping.id}`, + { + body: JSON.stringify({ + rules: { + username: 'user', + }, + roleType: 'admin', + groups: [], + allGroups: true, + authProvider: [ANY_AUTH_PROVIDER, 'other_auth'], + }), + } + ); + await nextTick(); + + expect(navigateToUrl).toHaveBeenCalled(); + }); + + it('handles error', async () => { + http.post.mockReturnValue(Promise.reject('this is an error')); + RoleMappingsLogic.actions.handleSaveMapping(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + }); + }); + + describe('handleDeleteMapping', () => { + let confirmSpy: any; + + beforeEach(() => { + confirmSpy = jest.spyOn(window, 'confirm'); + confirmSpy.mockImplementation(jest.fn(() => true)); + }); + + afterEach(() => { + confirmSpy.mockRestore(); + }); + + it('returns when no mapping', () => { + RoleMappingsLogic.actions.handleDeleteMapping(); + + expect(http.delete).not.toHaveBeenCalled(); + }); + + it('calls API and navigates', async () => { + RoleMappingsLogic.actions.setRoleMappingData(mappingServerProps); + http.delete.mockReturnValue(Promise.resolve({})); + RoleMappingsLogic.actions.handleDeleteMapping(); + + expect(http.delete).toHaveBeenCalledWith( + `/api/workplace_search/org/role_mappings/${wsRoleMapping.id}` + ); + await nextTick(); + + expect(navigateToUrl).toHaveBeenCalled(); + }); + + it('handles error', async () => { + RoleMappingsLogic.actions.setRoleMappingData(mappingServerProps); + http.delete.mockReturnValue(Promise.reject('this is an error')); + RoleMappingsLogic.actions.handleDeleteMapping(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); + }); + + it('will do nothing if not confirmed', async () => { + RoleMappingsLogic.actions.setRoleMappingData(mappingServerProps); + window.confirm = () => false; + RoleMappingsLogic.actions.handleDeleteMapping(); + + expect(http.delete).not.toHaveBeenCalled(); + await nextTick(); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts new file mode 100644 index 0000000000000..6fc3867d7ab1e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts @@ -0,0 +1,311 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kea, MakeLogicType } from 'kea'; + +import { clearFlashMessages, flashAPIErrors } from '../../../shared/flash_messages'; +import { HttpLogic } from '../../../shared/http'; +import { KibanaLogic } from '../../../shared/kibana'; +import { AttributeName } from '../../../shared/role_mapping/attribute_selector'; +import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants'; +import { ROLE_MAPPINGS_PATH } from '../../routes'; +import { RoleGroup, WSRoleMapping, Role } from '../../types'; + +import { DELETE_ROLE_MAPPING_MESSAGE, DEFAULT_GROUP_NAME } from './constants'; + +interface RoleMappingsServerDetails { + multipleAuthProvidersConfig: boolean; + roleMappings: WSRoleMapping[]; +} + +interface RoleMappingServerDetails { + attributes: string[]; + authProviders: string[]; + availableGroups: RoleGroup[]; + elasticsearchRoles: string[]; + multipleAuthProvidersConfig: boolean; + roleMapping?: WSRoleMapping; +} + +interface RoleMappingsActions { + setRoleMappingsData(data: RoleMappingsServerDetails): RoleMappingsServerDetails; + setRoleMappingData(data: RoleMappingServerDetails): RoleMappingServerDetails; + handleRoleChange(roleType: Role): { roleType: Role }; + handleAllGroupsSelectionChange(selected: boolean): { selected: boolean }; + handleAttributeSelectorChange( + value: AttributeName, + firstElasticsearchRole: string + ): { value: AttributeName; firstElasticsearchRole: string }; + handleAttributeValueChange(value: string): { value: string }; + handleGroupSelectionChange( + groupId: string, + selected: boolean + ): { groupId: string; selected: boolean }; + handleAuthProviderChange(value: string[]): { value: string[] }; + resetState(): void; + initializeRoleMapping(roleId?: string): { roleId?: string }; + handleSaveMapping(): void; + handleDeleteMapping(): void; + initializeRoleMappings(): void; +} + +interface RoleMappingsValues { + attributes: string[]; + availableAuthProviders: string[]; + elasticsearchRoles: string[]; + roleMapping: WSRoleMapping | null; + roleMappings: WSRoleMapping[]; + roleType: Role; + attributeValue: string; + attributeName: AttributeName; + dataLoading: boolean; + multipleAuthProvidersConfig: boolean; + availableGroups: RoleGroup[]; + selectedGroups: Set; + includeInAllGroups: boolean; + selectedAuthProviders: string[]; +} + +const getFirstAttributeName = (roleMapping: WSRoleMapping): AttributeName => + Object.entries(roleMapping.rules)[0][0] as AttributeName; +const getFirstAttributeValue = (roleMapping: WSRoleMapping): string => + Object.entries(roleMapping.rules)[0][1] as string; + +export const RoleMappingsLogic = kea>({ + actions: { + setRoleMappingsData: (data: RoleMappingsServerDetails) => data, + setRoleMappingData: (data: RoleMappingServerDetails) => data, + handleRoleChange: (roleType: Role) => ({ roleType }), + handleGroupSelectionChange: (groupId: string, selected: boolean) => ({ groupId, selected }), + handleAllGroupsSelectionChange: (selected: boolean) => ({ selected }), + handleAttributeSelectorChange: (value: string, firstElasticsearchRole: string) => ({ + value, + firstElasticsearchRole, + }), + handleAttributeValueChange: (value: string) => ({ value }), + handleAuthProviderChange: (value: string[]) => ({ value }), + resetState: () => true, + initializeRoleMapping: (roleId?: string) => ({ roleId }), + handleSaveMapping: () => true, + handleDeleteMapping: () => true, + initializeRoleMappings: () => true, + }, + reducers: { + dataLoading: [ + true, + { + setRoleMappingsData: () => false, + setRoleMappingData: () => false, + resetState: () => true, + }, + ], + roleMappings: [ + [], + { + setRoleMappingsData: (_, { roleMappings }) => roleMappings, + resetState: () => [], + }, + ], + attributes: [ + [], + { + setRoleMappingData: (_, { attributes }) => attributes, + }, + ], + availableGroups: [ + [], + { + setRoleMappingData: (_, { availableGroups }) => availableGroups, + }, + ], + selectedGroups: [ + new Set(), + { + setRoleMappingData: (_, { roleMapping, availableGroups }) => + roleMapping + ? new Set(roleMapping.groups.map((group) => group.id)) + : new Set( + availableGroups + .filter((group) => group.name === DEFAULT_GROUP_NAME) + .map((group) => group.id) + ), + handleGroupSelectionChange: (groups, { groupId, selected }) => { + const newSelectedGroupNames = new Set(groups as Set); + if (selected) { + newSelectedGroupNames.add(groupId); + } else { + newSelectedGroupNames.delete(groupId); + } + return newSelectedGroupNames; + }, + }, + ], + includeInAllGroups: [ + false, + { + setRoleMappingData: (_, { roleMapping }) => (roleMapping ? roleMapping.allGroups : false), + handleAllGroupsSelectionChange: (_, { selected }) => selected, + }, + ], + elasticsearchRoles: [ + [], + { + setRoleMappingData: (_, { elasticsearchRoles }) => elasticsearchRoles, + }, + ], + roleMapping: [ + null, + { + setRoleMappingData: (_, { roleMapping }) => roleMapping || null, + resetState: () => null, + }, + ], + roleType: [ + 'admin', + { + setRoleMappingData: (_, { roleMapping }) => + roleMapping ? (roleMapping.roleType as Role) : 'admin', + handleRoleChange: (_, { roleType }) => roleType, + }, + ], + attributeValue: [ + '', + { + setRoleMappingData: (_, { roleMapping }) => + roleMapping ? getFirstAttributeValue(roleMapping) : '', + handleAttributeSelectorChange: (_, { value, firstElasticsearchRole }) => + value === 'role' ? firstElasticsearchRole : '', + handleAttributeValueChange: (_, { value }) => value, + resetState: () => '', + }, + ], + attributeName: [ + 'username', + { + setRoleMappingData: (_, { roleMapping }) => + roleMapping ? getFirstAttributeName(roleMapping) : 'username', + handleAttributeSelectorChange: (_, { value }) => value, + resetState: () => 'username', + }, + ], + availableAuthProviders: [ + [], + { + setRoleMappingData: (_, { authProviders }) => authProviders, + }, + ], + multipleAuthProvidersConfig: [ + false, + { + setRoleMappingsData: (_, { multipleAuthProvidersConfig }) => multipleAuthProvidersConfig, + setRoleMappingData: (_, { multipleAuthProvidersConfig }) => multipleAuthProvidersConfig, + resetState: () => false, + }, + ], + selectedAuthProviders: [ + [ANY_AUTH_PROVIDER], + { + handleAuthProviderChange: (previous, { value }) => { + const previouslyContainedAny = previous.includes(ANY_AUTH_PROVIDER); + const newSelectionsContainAny = value.includes(ANY_AUTH_PROVIDER); + + if (value.length < 1) return [ANY_AUTH_PROVIDER]; + if (value.length === 1) return value; + if (!newSelectionsContainAny) return value; + if (previouslyContainedAny) return value.filter((v) => v !== ANY_AUTH_PROVIDER); + return [ANY_AUTH_PROVIDER]; + }, + setRoleMappingData: (_, { roleMapping }) => + roleMapping ? roleMapping.authProvider : [ANY_AUTH_PROVIDER], + }, + ], + }, + listeners: ({ actions, values }) => ({ + initializeRoleMappings: async () => { + const { http } = HttpLogic.values; + const route = '/api/workplace_search/org/role_mappings'; + + try { + const response = await http.get(route); + actions.setRoleMappingsData(response); + } catch (e) { + flashAPIErrors(e); + } + }, + initializeRoleMapping: async ({ roleId }) => { + const { http } = HttpLogic.values; + const { navigateToUrl } = KibanaLogic.values; + const route = roleId + ? `/api/workplace_search/org/role_mappings/${roleId}` + : '/api/workplace_search/org/role_mappings/new'; + + try { + const response = await http.get(route); + actions.setRoleMappingData(response); + } catch (e) { + if (e.status === 404) { + navigateToUrl(ROLE_MAPPINGS_PATH); + } + flashAPIErrors(e); + } + }, + handleDeleteMapping: async () => { + const { http } = HttpLogic.values; + const { navigateToUrl } = KibanaLogic.values; + const { roleMapping } = values; + if (!roleMapping) { + return; + } + const route = `/api/workplace_search/org/role_mappings/${roleMapping.id}`; + if (window.confirm(DELETE_ROLE_MAPPING_MESSAGE)) { + try { + await http.delete(route); + navigateToUrl(ROLE_MAPPINGS_PATH); + } catch (e) { + flashAPIErrors(e); + } + } + }, + handleSaveMapping: async () => { + const { http } = HttpLogic.values; + const { navigateToUrl } = KibanaLogic.values; + const { + attributeName, + attributeValue, + roleType, + roleMapping, + selectedGroups, + includeInAllGroups, + selectedAuthProviders, + } = values; + + const body = JSON.stringify({ + rules: { + [attributeName]: attributeValue, + }, + roleType, + groups: includeInAllGroups ? [] : Array.from(selectedGroups), + allGroups: includeInAllGroups, + authProvider: selectedAuthProviders, + }); + + const request = !roleMapping + ? http.post('/api/workplace_search/org/role_mappings', { body }) + : http.put(`/api/workplace_search/org/role_mappings/${roleMapping.id}`, { body }); + + try { + await request; + navigateToUrl(ROLE_MAPPINGS_PATH); + } catch (e) { + flashAPIErrors(e); + } + }, + resetState: () => { + clearFlashMessages(); + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_router.test.tsx new file mode 100644 index 0000000000000..e9fc40ba1dbb4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_router.test.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { Route, Switch } from 'react-router-dom'; + +import { shallow } from 'enzyme'; + +import { RoleMapping } from './role_mapping'; +import { RoleMappings } from './role_mappings'; +import { RoleMappingsRouter } from './role_mappings_router'; + +describe('RoleMappingsRouter', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(Switch)).toHaveLength(1); + expect(wrapper.find(Route)).toHaveLength(3); + expect(wrapper.find(RoleMapping)).toHaveLength(2); + expect(wrapper.find(RoleMappings)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_router.tsx new file mode 100644 index 0000000000000..fa5ab12c8afc0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_router.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { Route, Switch } from 'react-router-dom'; + +import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { NAV } from '../../constants'; +import { ROLE_MAPPING_NEW_PATH, ROLE_MAPPING_PATH, ROLE_MAPPINGS_PATH } from '../../routes'; + +import { RoleMapping } from './role_mapping'; +import { RoleMappings } from './role_mappings'; + +export const RoleMappingsRouter: React.FC = () => ( + <> + + + + + + + + + + + + + +); diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/index.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/index.ts index cc6226e340653..a21ffac02a48c 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/index.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/index.ts @@ -9,6 +9,7 @@ import { RouteDependencies } from '../../plugin'; import { registerGroupsRoutes } from './groups'; import { registerOverviewRoute } from './overview'; +import { registerRoleMappingsRoutes } from './role_mappings'; import { registerSecurityRoutes } from './security'; import { registerSettingsRoutes } from './settings'; import { registerSourcesRoutes } from './sources'; @@ -16,6 +17,7 @@ import { registerSourcesRoutes } from './sources'; export const registerWorkplaceSearchRoutes = (dependencies: RouteDependencies) => { registerOverviewRoute(dependencies); registerGroupsRoutes(dependencies); + registerRoleMappingsRoutes(dependencies); registerSourcesRoutes(dependencies); registerSettingsRoutes(dependencies); registerSecurityRoutes(dependencies); diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts new file mode 100644 index 0000000000000..0dade134767e4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.test.ts @@ -0,0 +1,154 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; + +import { + registerOrgRoleMappingsRoute, + registerOrgRoleMappingRoute, + registerOrgNewRoleMappingRoute, +} from './role_mappings'; + +describe('role mappings routes', () => { + describe('GET /api/workplace_search/org/role_mappings', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'get', + path: '/api/workplace_search/org/role_mappings', + }); + + registerOrgRoleMappingsRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/role_mappings/collection', + }); + }); + }); + + describe('POST /api/workplace_search/org/role_mappings', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'post', + path: '/api/workplace_search/org/role_mappings', + }); + + registerOrgRoleMappingsRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/role_mappings/collection', + }); + }); + }); + + describe('GET /api/workplace_search/org/role_mappings/{id}', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'get', + path: '/api/workplace_search/org/role_mappings/{id}', + }); + + registerOrgRoleMappingRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/role_mappings/:id', + }); + }); + }); + + describe('PUT /api/workplace_search/org/role_mappings/{id}', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'put', + path: '/api/workplace_search/org/role_mappings/{id}', + }); + + registerOrgRoleMappingRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/role_mappings/:id', + }); + }); + }); + + describe('DELETE /api/workplace_search/org/role_mappings/{id}', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'delete', + path: '/api/workplace_search/org/role_mappings/{id}', + }); + + registerOrgRoleMappingRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/role_mappings/:id', + }); + }); + }); + + describe('GET /api/workplace_search/org/role_mappings/new', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'get', + path: '/api/workplace_search/org/role_mappings/new', + }); + + registerOrgNewRoleMappingRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/role_mappings/new', + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts new file mode 100644 index 0000000000000..8c7792f56fd6c --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/role_mappings.ts @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; + +import { RouteDependencies } from '../../plugin'; + +const roleMappingBaseSchema = { + rules: schema.recordOf(schema.string(), schema.string()), + roleType: schema.string(), + groups: schema.arrayOf(schema.string()), + allGroups: schema.boolean(), + authProvider: schema.arrayOf(schema.string()), +}; + +export function registerOrgRoleMappingsRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/workplace_search/org/role_mappings', + validate: false, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/ws/org/role_mappings/collection', + }) + ); + + router.post( + { + path: '/api/workplace_search/org/role_mappings', + validate: { + body: schema.object(roleMappingBaseSchema), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/ws/org/role_mappings/collection', + }) + ); +} + +export function registerOrgRoleMappingRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/workplace_search/org/role_mappings/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/ws/org/role_mappings/:id', + }) + ); + + router.put( + { + path: '/api/workplace_search/org/role_mappings/{id}', + validate: { + body: schema.object({ + ...roleMappingBaseSchema, + id: schema.string(), + }), + params: schema.object({ + id: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/ws/org/role_mappings/:id', + }) + ); + + router.delete( + { + path: '/api/workplace_search/org/role_mappings/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/ws/org/role_mappings/:id', + }) + ); +} + +export function registerOrgNewRoleMappingRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/workplace_search/org/role_mappings/new', + validate: false, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/ws/org/role_mappings/new', + }) + ); +} + +export const registerRoleMappingsRoutes = (dependencies: RouteDependencies) => { + registerOrgRoleMappingsRoute(dependencies); + registerOrgRoleMappingRoute(dependencies); + registerOrgNewRoleMappingRoute(dependencies); +};