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); +};