Skip to content

Commit

Permalink
[Workplace Search] Role Mappings to Kibana (elastic#93123)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
scottybollinger and kibanamachine committed Mar 2, 2021
1 parent 88aadc3 commit ae6eaa0
Show file tree
Hide file tree
Showing 17 changed files with 1,626 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -43,9 +42,7 @@ export const WorkplaceSearchNav: React.FC<Props> = ({
<SideNavLink to={GROUPS_PATH} subNav={groupsSubNav}>
{NAV.GROUPS}
</SideNavLink>
<SideNavLink isExternal to={getWorkplaceSearchUrl(`#${ROLE_MAPPINGS_PATH}`)}>
{NAV.ROLE_MAPPINGS}
</SideNavLink>
<SideNavLink to={ROLE_MAPPINGS_PATH}>{NAV.ROLE_MAPPINGS}</SideNavLink>
<SideNavLink to={SECURITY_PATH}>{NAV.SECURITY}</SideNavLink>
<SideNavLink subNav={settingsSubNav} to={ORG_SETTINGS_PATH}>
{NAV.SETTINGS}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -111,6 +113,11 @@ export const WorkplaceSearchConfigured: React.FC<InitialAppData> = (props) => {
<GroupsRouter />
</Layout>
</Route>
<Route path={ROLE_MAPPINGS_PATH}>
<Layout navigation={<WorkplaceSearchNav />} restrictWidth readOnlyMode={readOnlyMode}>
<RoleMappingsRouter />
</Layout>
</Route>
<Route path={SECURITY_PATH}>
<Layout navigation={<WorkplaceSearchNav />} restrictWidth readOnlyMode={readOnlyMode}>
<Security />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export interface Meta {
page: MetaPage;
}

export type Role = 'admin' | 'user';

export interface Group {
id: string;
name: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -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.',
}
);
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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(<RoleMapping />);

expect(wrapper.find(AttributeSelector)).toHaveLength(1);
expect(wrapper.find(RoleSelector)).toHaveLength(2);
});

it('returns Loading when loading', () => {
setMockValues({ ...mockValues, dataLoading: true });
const wrapper = shallow(<RoleMapping />);

expect(wrapper.find(Loading)).toHaveLength(1);
});

it('hides DeleteMappingCallout for new mapping', () => {
const wrapper = shallow(<RoleMapping isNew />);

expect(wrapper.find(DeleteMappingCallout)).toHaveLength(0);
});

it('handles group checkbox click', () => {
const wrapper = shallow(<RoleMapping />);
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(<RoleMapping />);
wrapper
.find(EuiCheckbox)
.last()
.simulate('change', { target: { checked: true } });

expect(handleAllGroupsSelectionChange).toHaveBeenCalledWith(true);
});
});
Loading

0 comments on commit ae6eaa0

Please sign in to comment.