[App Search] Role mappings migration part 3 (#94763)

* Remove validaition of ID property in route body

The ID is inferred from the param in the URL. This was fixed in the logic file but the server route was never updated

* Add RoleMappings component

- ROLE_MAPPINGS_TITLE was moved to a shared constant in an earlier PR
- Also removing redundant exports of interface

* Add RoleMapping component

- Also removing redundant export of interface from AppLogic

* Add RoleMappingsRouter

ROLE_MAPPINGS_TITLE was moved to a shared constant in an earlier PR

# Conflicts:
#	x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/index.ts

* Add route and update link in navigation

* Remove unused translations

* Change casing

Co-authored-by: Constance <constancecchen@users.noreply.github.com>

* Change casing

Co-authored-by: Constance <constancecchen@users.noreply.github.com>

* Change casing

Co-authored-by: Constance <constancecchen@users.noreply.github.com>

* Add ability test

* Refactor conditional constants

* Refactor role type constants

* Remove EuiPageContent

* Refactor action mocks

Co-authored-by: Constance <constancecchen@users.noreply.github.com>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Scotty Bollinger 2021-03-23 14:18:11 -05:00 committed by GitHub
parent 685aa20ba6
commit 29ee309dd8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 841 additions and 27 deletions

View file

@ -13,7 +13,7 @@ import { ConfiguredLimits, Account, Role } from './types';
import { getRoleAbilities } from './utils/role';
export interface AppValues {
interface AppValues {
ilmEnabled: boolean;
configuredLimits: ConfiguredLimits;
account: Account;

View file

@ -7,9 +7,32 @@
import { i18n } from '@kbn/i18n';
export const ROLE_MAPPINGS_TITLE = i18n.translate(
'xpack.enterpriseSearch.appSearch.roleMappings.title',
{ defaultMessage: 'Role Mappings' }
import { AdvanceRoleType } from '../../types';
export const SAVE_ROLE_MAPPING = i18n.translate(
'xpack.enterpriseSearch.appSearch.roleMapping.saveRoleMappingButtonLabel',
{ defaultMessage: 'Save role mapping' }
);
export const UPDATE_ROLE_MAPPING = i18n.translate(
'xpack.enterpriseSearch.appSearch.roleMapping.updateRoleMappingButtonLabel',
{ defaultMessage: 'Update role mapping' }
);
export const ADD_ROLE_MAPPING_TITLE = i18n.translate(
'xpack.enterpriseSearch.appSearch.roleMapping.newRoleMappingTitle',
{ defaultMessage: 'Add role mapping' }
);
export const MANAGE_ROLE_MAPPING_TITLE = i18n.translate(
'xpack.enterpriseSearch.appSearch.roleMapping.manageRoleMappingTitle',
{ defaultMessage: 'Manage role mapping' }
);
export const EMPTY_ROLE_MAPPINGS_BODY = i18n.translate(
'xpack.enterpriseSearch.appSearch.roleMapping.emptyRoleMappingsBody',
{
defaultMessage:
'All users who successfully authenticate will be assigned the Owner role and have access to all engines. Add a new role to override the default.',
}
);
export const DELETE_ROLE_MAPPING_MESSAGE = i18n.translate(
@ -40,3 +63,146 @@ export const ROLE_MAPPING_UPDATED_MESSAGE = i18n.translate(
defaultMessage: 'Role mapping successfully updated.',
}
);
export const ROLE_MAPPINGS_ENGINE_ACCESS_HEADING = i18n.translate(
'xpack.enterpriseSearch.appSearch.roleMappingsEngineAccessHeading',
{
defaultMessage: 'Engine access',
}
);
export const ROLE_MAPPINGS_RESET_BUTTON = i18n.translate(
'xpack.enterpriseSearch.appSearch.roleMappingsResetButton',
{
defaultMessage: 'Reset mappings',
}
);
export const ROLE_MAPPINGS_RESET_CONFIRM_TITLE = i18n.translate(
'xpack.enterpriseSearch.appSearch.roleMappingsResetConfirmTitle',
{
defaultMessage: 'Are you sure you want to reset role mappings?',
}
);
export const ROLE_MAPPINGS_RESET_CONFIRM_BUTTON = i18n.translate(
'xpack.enterpriseSearch.appSearch.roleMappingsResetConfirmButton',
{
defaultMessage: 'Reset role mappings',
}
);
export const ROLE_MAPPINGS_RESET_CANCEL_BUTTON = i18n.translate(
'xpack.enterpriseSearch.appSearch.roleMappingsResetCancelButton',
{
defaultMessage: 'Cancel',
}
);
export const DEV_ROLE_TYPE_DESCRIPTION = i18n.translate(
'xpack.enterpriseSearch.appSearch.DEV_ROLE_TYPE_DESCRIPTION',
{
defaultMessage: 'Devs can manage all aspects of an engine.',
}
);
export const EDITOR_ROLE_TYPE_DESCRIPTION = i18n.translate(
'xpack.enterpriseSearch.appSearch.editorRoleTypeDescription',
{
defaultMessage: 'Editors can manage search settings.',
}
);
export const ANALYST_ROLE_TYPE_DESCRIPTION = i18n.translate(
'xpack.enterpriseSearch.appSearch.analystRoleTypeDescription',
{
defaultMessage: 'Analysts can only view documents, query tester, and analytics.',
}
);
export const OWNER_ROLE_TYPE_DESCRIPTION = i18n.translate(
'xpack.enterpriseSearch.appSearch.ownerRoleTypeDescription',
{
defaultMessage:
'Owners can do anything. There can be many owners on the account, but there must be at least one owner at any time.',
}
);
export const ADMIN_ROLE_TYPE_DESCRIPTION = i18n.translate(
'xpack.enterpriseSearch.appSearch.adminRoleTypeDescription',
{
defaultMessage: 'Admins can do anything, except manage account settings.',
}
);
export const ADVANCED_ROLE_SELECTORS_TITLE = i18n.translate(
'xpack.enterpriseSearch.appSearch.advancedRoleSelectorsTitle',
{
defaultMessage: 'Full or limited engine access',
}
);
export const ROLE_TITLE = i18n.translate('xpack.enterpriseSearch.appSearch.roleTitle', {
defaultMessage: 'Role',
});
export const FULL_ENGINE_ACCESS_TITLE = i18n.translate(
'xpack.enterpriseSearch.appSearch.fullEngineAccessTitle',
{
defaultMessage: 'Full engine access',
}
);
export const FULL_ENGINE_ACCESS_DESCRIPTION = i18n.translate(
'xpack.enterpriseSearch.appSearch.fullEngineAccessDescription',
{
defaultMessage: 'Access to all current and future engines.',
}
);
export const LIMITED_ENGINE_ACCESS_TITLE = i18n.translate(
'xpack.enterpriseSearch.appSearch.limitedEngineAccessTitle',
{
defaultMessage: 'Limited engine access',
}
);
export const LIMITED_ENGINE_ACCESS_DESCRIPTION = i18n.translate(
'xpack.enterpriseSearch.appSearch.limitedEngineAccessDescription',
{
defaultMessage: 'Limit user access to specific engines:',
}
);
export const ENGINE_ACCESS_TITLE = i18n.translate(
'xpack.enterpriseSearch.appSearch.engineAccessTitle',
{
defaultMessage: 'Engine access',
}
);
export const ADVANCED_ROLE_TYPES = [
{
type: 'dev',
description: DEV_ROLE_TYPE_DESCRIPTION,
},
{
type: 'editor',
description: EDITOR_ROLE_TYPE_DESCRIPTION,
},
{
type: 'analyst',
description: ANALYST_ROLE_TYPE_DESCRIPTION,
},
] as AdvanceRoleType[];
export const STANDARD_ROLE_TYPES = [
{
type: 'owner',
description: OWNER_ROLE_TYPE_DESCRIPTION,
},
{
type: 'admin',
description: ADMIN_ROLE_TYPE_DESCRIPTION,
},
] as AdvanceRoleType[];

View file

@ -5,4 +5,4 @@
* 2.0.
*/
export { ROLE_MAPPINGS_TITLE } from './constants';
export { RoleMappingsRouter } from './role_mappings_router';

View file

@ -0,0 +1,106 @@
/*
* 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 { DEFAULT_INITIAL_APP_DATA } from '../../../../../common/__mocks__';
import { setMockActions, setMockValues } from '../../../__mocks__';
import { engines } from '../../__mocks__/engines.mock';
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 { asRoleMapping } from '../../../shared/role_mapping/__mocks__/roles';
import { RoleMapping } from './role_mapping';
describe('RoleMapping', () => {
const mockRole = DEFAULT_INITIAL_APP_DATA.appSearch.role;
const actions = {
initializeRoleMappings: jest.fn(),
initializeRoleMapping: jest.fn(),
handleSaveMapping: jest.fn(),
handleEngineSelectionChange: jest.fn(),
handleAccessAllEnginesChange: jest.fn(),
handleAttributeValueChange: jest.fn(),
handleAttributeSelectorChange: jest.fn(),
handleDeleteMapping: jest.fn(),
handleRoleChange: jest.fn(),
handleAuthProviderChange: jest.fn(),
resetState: jest.fn(),
};
const mockValues = {
attributes: [],
elasticsearchRoles: [],
hasAdvancedRoles: true,
dataLoading: false,
roleType: 'admin',
roleMappings: [asRoleMapping],
attributeValue: '',
attributeName: 'username',
availableEngines: engines,
selectedEngines: new Set(),
accessAllEngines: false,
availableAuthProviders: [],
multipleAuthProvidersConfig: true,
selectedAuthProviders: [],
myRole: {
availableRoleTypes: mockRole.ability.availableRoleTypes,
},
};
beforeEach(() => {
setMockActions(actions);
setMockValues(mockValues);
});
it('renders', () => {
const wrapper = shallow(<RoleMapping />);
expect(wrapper.find(AttributeSelector)).toHaveLength(1);
expect(wrapper.find(RoleSelector)).toHaveLength(5);
});
it('returns Loading when loading', () => {
setMockValues({ ...mockValues, dataLoading: true });
const wrapper = shallow(<RoleMapping />);
expect(wrapper.find(Loading)).toHaveLength(1);
});
it('renders DeleteMappingCallout for existing mapping', () => {
setMockValues({ ...mockValues, roleMapping: asRoleMapping });
const wrapper = shallow(<RoleMapping />);
expect(wrapper.find(DeleteMappingCallout)).toHaveLength(1);
});
it('hides DeleteMappingCallout for new mapping', () => {
const wrapper = shallow(<RoleMapping isNew />);
expect(wrapper.find(DeleteMappingCallout)).toHaveLength(0);
});
it('handles engine checkbox click', () => {
const wrapper = shallow(<RoleMapping />);
wrapper
.find(EuiCheckbox)
.first()
.simulate('change', { target: { checked: true } });
expect(actions.handleEngineSelectionChange).toHaveBeenCalledWith(engines[0].name, true);
});
});

View file

@ -0,0 +1,246 @@
/*
* 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,
EuiPageContentBody,
EuiPageHeader,
EuiPanel,
EuiRadio,
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
import { FlashMessages } from '../../../shared/flash_messages';
import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
import { Loading } from '../../../shared/loading';
import {
AttributeSelector,
DeleteMappingCallout,
RoleSelector,
} from '../../../shared/role_mapping';
import { ROLE_MAPPINGS_TITLE } from '../../../shared/role_mapping/constants';
import { AppLogic } from '../../app_logic';
import { roleHasScopedEngines } from '../../utils/role/has_scoped_engines';
import { Engine } from '../engine/types';
import {
SAVE_ROLE_MAPPING,
UPDATE_ROLE_MAPPING,
ADD_ROLE_MAPPING_TITLE,
MANAGE_ROLE_MAPPING_TITLE,
ADVANCED_ROLE_TYPES,
STANDARD_ROLE_TYPES,
ADVANCED_ROLE_SELECTORS_TITLE,
ROLE_TITLE,
FULL_ENGINE_ACCESS_TITLE,
FULL_ENGINE_ACCESS_DESCRIPTION,
LIMITED_ENGINE_ACCESS_TITLE,
LIMITED_ENGINE_ACCESS_DESCRIPTION,
ENGINE_ACCESS_TITLE,
} from './constants';
import { RoleMappingsLogic } from './role_mappings_logic';
interface RoleMappingProps {
isNew?: boolean;
}
export const RoleMapping: React.FC<RoleMappingProps> = ({ isNew }) => {
const { roleId } = useParams() as { roleId: string };
const { myRole } = useValues(AppLogic);
const {
handleAccessAllEnginesChange,
handleAttributeSelectorChange,
handleAttributeValueChange,
handleAuthProviderChange,
handleDeleteMapping,
handleEngineSelectionChange,
handleRoleChange,
handleSaveMapping,
initializeRoleMapping,
resetState,
} = useActions(RoleMappingsLogic);
const {
accessAllEngines,
attributeName,
attributeValue,
attributes,
availableAuthProviders,
availableEngines,
dataLoading,
elasticsearchRoles,
hasAdvancedRoles,
multipleAuthProvidersConfig,
roleMapping,
roleType,
selectedEngines,
selectedAuthProviders,
} = useValues(RoleMappingsLogic);
useEffect(() => {
initializeRoleMapping(roleId);
return resetState;
}, []);
if (dataLoading) return <Loading />;
const SAVE_ROLE_MAPPING_LABEL = isNew ? SAVE_ROLE_MAPPING : UPDATE_ROLE_MAPPING;
const TITLE = isNew ? ADD_ROLE_MAPPING_TITLE : MANAGE_ROLE_MAPPING_TITLE;
const saveRoleMappingButton = (
<EuiButton onClick={handleSaveMapping} fill>
{SAVE_ROLE_MAPPING_LABEL}
</EuiButton>
);
const engineSelector = (engine: Engine) => (
<EuiCheckbox
key={engine.name}
name={engine.name}
id={`engine_option_${engine.name}`}
checked={selectedEngines.has(engine.name)}
onChange={(e) => {
handleEngineSelectionChange(engine.name, e.target.checked);
}}
label={engine.name}
/>
);
const advancedRoleSelectors = (
<>
<EuiSpacer />
<EuiTitle size="xs">
<h4>{ADVANCED_ROLE_SELECTORS_TITLE}</h4>
</EuiTitle>
<EuiSpacer />
{ADVANCED_ROLE_TYPES.map(({ type, description }) => (
<RoleSelector
key={type}
disabled={!myRole.availableRoleTypes.includes(type)}
roleType={roleType}
roleTypeOption={type}
description={description}
onChange={handleRoleChange}
/>
))}
</>
);
return (
<>
<SetPageChrome trail={[ROLE_MAPPINGS_TITLE, TITLE]} />
<EuiPageHeader rightSideItems={[saveRoleMappingButton]} pageTitle={TITLE} />
<EuiSpacer />
<EuiPageContentBody>
<FlashMessages />
<AttributeSelector
attributeName={attributeName}
attributeValue={attributeValue}
attributes={attributes}
availableAuthProviders={availableAuthProviders}
elasticsearchRoles={elasticsearchRoles}
selectedAuthProviders={selectedAuthProviders}
disabled={!!roleMapping}
handleAttributeSelectorChange={handleAttributeSelectorChange}
handleAttributeValueChange={handleAttributeValueChange}
handleAuthProviderChange={handleAuthProviderChange}
multipleAuthProvidersConfig={multipleAuthProvidersConfig}
/>
<EuiSpacer />
<EuiFlexGroup alignItems="stretch">
<EuiFlexItem>
<EuiPanel paddingSize="l">
<EuiTitle size="s">
<h3>{ROLE_TITLE}</h3>
</EuiTitle>
<EuiSpacer />
<EuiTitle size="xs">
<h4>{FULL_ENGINE_ACCESS_TITLE}</h4>
</EuiTitle>
<EuiSpacer />
export{' '}
{STANDARD_ROLE_TYPES.map(({ type, description }) => (
<RoleSelector
key={type}
roleType={roleType}
onChange={handleRoleChange}
roleTypeOption={type}
description={description}
/>
))}
{hasAdvancedRoles && advancedRoleSelectors}
</EuiPanel>
</EuiFlexItem>
{hasAdvancedRoles && (
<EuiFlexItem>
<EuiPanel paddingSize="l">
<EuiTitle size="s">
<h3>{ENGINE_ACCESS_TITLE}</h3>
</EuiTitle>
<EuiSpacer />
<EuiFormRow>
<EuiRadio
id="accessAllEngines"
disabled={!roleHasScopedEngines(roleType)}
checked={accessAllEngines}
onChange={handleAccessAllEnginesChange}
label={
<>
<EuiTitle size="xs">
<h4>{FULL_ENGINE_ACCESS_TITLE}</h4>
</EuiTitle>
<p>{FULL_ENGINE_ACCESS_DESCRIPTION}</p>
</>
}
/>
</EuiFormRow>
<EuiFormRow>
<>
<EuiRadio
id="selectEngines"
disabled={!roleHasScopedEngines(roleType)}
checked={!accessAllEngines}
onChange={handleAccessAllEnginesChange}
label={
<>
<EuiTitle size="xs">
<h4>{LIMITED_ENGINE_ACCESS_TITLE}</h4>
</EuiTitle>
<p>{LIMITED_ENGINE_ACCESS_DESCRIPTION}</p>
</>
}
/>
{!accessAllEngines && (
<div className="engines-list">
{availableEngines.map((engine) => engineSelector(engine))}
</div>
)}
</>
</EuiFormRow>
</EuiPanel>
</EuiFlexItem>
)}
</EuiFlexGroup>
<EuiSpacer />
{roleMapping && <DeleteMappingCallout handleDeleteMapping={handleDeleteMapping} />}
</EuiPageContentBody>
</>
);
};

View file

@ -0,0 +1,88 @@
/*
* 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, { MouseEvent } from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
import { EuiEmptyPrompt, EuiConfirmModal, EuiPageHeader } 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 handleResetMappings = jest.fn();
const mockValues = {
roleMappings: [wsRoleMapping],
dataLoading: false,
multipleAuthProvidersConfig: false,
};
beforeEach(() => {
setMockActions({
initializeRoleMappings,
handleResetMappings,
});
setMockValues(mockValues);
});
it('renders', () => {
const wrapper = shallow(<RoleMappings />);
expect(wrapper.find(RoleMappingsTable)).toHaveLength(1);
});
it('returns Loading when loading', () => {
setMockValues({ ...mockValues, dataLoading: true });
const wrapper = shallow(<RoleMappings />);
expect(wrapper.find(Loading)).toHaveLength(1);
});
it('renders empty state', () => {
setMockValues({ ...mockValues, roleMappings: [] });
const wrapper = shallow(<RoleMappings />);
expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1);
});
describe('resetMappingsWarningModal', () => {
let wrapper: ShallowWrapper;
beforeEach(() => {
wrapper = shallow(<RoleMappings />);
const button = wrapper.find(EuiPageHeader).prop('rightSideItems')![0] as any;
button.props.onClick();
});
it('renders reset warnings modal', () => {
expect(wrapper.find(EuiConfirmModal)).toHaveLength(1);
});
it('hides reset warnings modal', () => {
const modal = wrapper.find(EuiConfirmModal);
modal.prop('onCancel')();
expect(wrapper.find(EuiConfirmModal)).toHaveLength(0);
});
it('resets when confirmed', () => {
const event = {} as MouseEvent<HTMLButtonElement>;
const modal = wrapper.find(EuiConfirmModal);
modal.prop('onConfirm')!(event);
expect(handleResetMappings).toHaveBeenCalled();
});
});
});

View file

@ -0,0 +1,139 @@
/*
* 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, useState } from 'react';
import { useActions, useValues } from 'kea';
import {
EuiButton,
EuiConfirmModal,
EuiEmptyPrompt,
EuiOverlayMask,
EuiPageContent,
EuiPageContentBody,
EuiPageHeader,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { FlashMessages } from '../../../shared/flash_messages';
import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
import { Loading } from '../../../shared/loading';
import { AddRoleMappingButton, RoleMappingsTable } from '../../../shared/role_mapping';
import {
EMPTY_ROLE_MAPPINGS_TITLE,
ROLE_MAPPINGS_TITLE,
ROLE_MAPPINGS_DESCRIPTION,
} from '../../../shared/role_mapping/constants';
import { ROLE_MAPPING_NEW_PATH } from '../../routes';
import {
ROLE_MAPPINGS_ENGINE_ACCESS_HEADING,
EMPTY_ROLE_MAPPINGS_BODY,
ROLE_MAPPINGS_RESET_BUTTON,
ROLE_MAPPINGS_RESET_CONFIRM_TITLE,
ROLE_MAPPINGS_RESET_CONFIRM_BUTTON,
ROLE_MAPPINGS_RESET_CANCEL_BUTTON,
} from './constants';
import { RoleMappingsLogic } from './role_mappings_logic';
import { generateRoleMappingPath } from './utils';
export const RoleMappings: React.FC = () => {
const { initializeRoleMappings, handleResetMappings, resetState } = useActions(RoleMappingsLogic);
const { roleMappings, multipleAuthProvidersConfig, dataLoading } = useValues(RoleMappingsLogic);
useEffect(() => {
initializeRoleMappings();
return resetState;
}, []);
const [isResetWarningVisible, setResetWarningVisibility] = useState(false);
const showWarning = () => setResetWarningVisibility(true);
const hideWarning = () => setResetWarningVisibility(false);
if (dataLoading) return <Loading />;
const RESET_MAPPINGS_WARNING_MODAL_BODY = (
<FormattedMessage
id="xpack.enterpriseSearch.appSearch.resetMappingsWarningModalBody"
defaultMessage="{strongText}, and all users who successfully authenticate will be assigned the Owner role and have access to all engines."
values={{
strongText: (
<strong>
{i18n.translate('xpack.enterpriseSearch.appSearch.resetMappingsWarningModalBodyBold', {
defaultMessage: 'All role mappings will be deleted',
})}
</strong>
),
}}
/>
);
const addMappingButton = <AddRoleMappingButton path={ROLE_MAPPING_NEW_PATH} />;
const roleMappingEmptyState = (
<EuiEmptyPrompt
iconType="usersRolesApp"
title={<h2>{EMPTY_ROLE_MAPPINGS_TITLE}</h2>}
body={<p>{EMPTY_ROLE_MAPPINGS_BODY}</p>}
actions={addMappingButton}
/>
);
const roleMappingsTable = (
<RoleMappingsTable
roleMappings={roleMappings}
accessItemKey="engines"
accessHeader={ROLE_MAPPINGS_ENGINE_ACCESS_HEADING}
addMappingButton={addMappingButton}
getRoleMappingPath={generateRoleMappingPath}
shouldShowAuthProvider={multipleAuthProvidersConfig}
/>
);
const resetMappings = (
<EuiButton size="s" color="danger" onClick={showWarning}>
{ROLE_MAPPINGS_RESET_BUTTON}
</EuiButton>
);
const resetMappingsWarningModal = isResetWarningVisible ? (
<EuiOverlayMask>
<EuiConfirmModal
onCancel={hideWarning}
onConfirm={() => handleResetMappings(hideWarning)}
title={ROLE_MAPPINGS_RESET_CONFIRM_TITLE}
cancelButtonText={ROLE_MAPPINGS_RESET_CANCEL_BUTTON}
confirmButtonText={ROLE_MAPPINGS_RESET_CONFIRM_BUTTON}
buttonColor="danger"
maxWidth={640}
>
<p>{RESET_MAPPINGS_WARNING_MODAL_BODY}</p>
</EuiConfirmModal>
</EuiOverlayMask>
) : null;
return (
<>
<SetPageChrome trail={[ROLE_MAPPINGS_TITLE]} />
<EuiPageHeader
rightSideItems={[resetMappings]}
pageTitle={ROLE_MAPPINGS_TITLE}
description={ROLE_MAPPINGS_DESCRIPTION}
/>
<EuiPageContent>
<EuiPageContentBody>
<FlashMessages />
{roleMappings.length === 0 ? roleMappingEmptyState : roleMappingsTable}
</EuiPageContentBody>
</EuiPageContent>
{resetMappingsWarningModal}
</>
);
};

View file

@ -48,7 +48,7 @@ const getFirstAttributeName = (roleMapping: ASRoleMapping) =>
const getFirstAttributeValue = (roleMapping: ASRoleMapping) =>
Object.entries(roleMapping.rules)[0][1] as AttributeName;
export interface RoleMappingsActions {
interface RoleMappingsActions {
handleAccessAllEnginesChange(): void;
handleAuthProviderChange(value: string[]): { value: string[] };
handleAttributeSelectorChange(
@ -74,7 +74,7 @@ export interface RoleMappingsActions {
setRoleMappingsData(data: RoleMappingsServerDetails): RoleMappingsServerDetails;
}
export interface RoleMappingsValues {
interface RoleMappingsValues {
accessAllEngines: boolean;
attributeName: AttributeName;
attributeValue: string;

View file

@ -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(<RoleMappingsRouter />);
expect(wrapper.find(Switch)).toHaveLength(1);
expect(wrapper.find(Route)).toHaveLength(3);
expect(wrapper.find(RoleMapping)).toHaveLength(2);
expect(wrapper.find(RoleMappings)).toHaveLength(1);
});
});

View file

@ -0,0 +1,29 @@
/*
* 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 { 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 = () => (
<Switch>
<Route exact path={ROLE_MAPPING_NEW_PATH}>
<RoleMapping isNew />
</Route>
<Route exact path={ROLE_MAPPINGS_PATH}>
<RoleMappings />
</Route>
<Route path={ROLE_MAPPING_PATH}>
<RoleMapping />
</Route>
</Switch>
);

View file

@ -25,6 +25,7 @@ import { EngineCreation } from './components/engine_creation';
import { EnginesOverview } from './components/engines';
import { ErrorConnecting } from './components/error_connecting';
import { MetaEngineCreation } from './components/meta_engine_creation';
import { RoleMappingsRouter } from './components/role_mappings';
import { SetupGuide } from './components/setup_guide';
import { AppSearch, AppSearchUnconfigured, AppSearchConfigured, AppSearchNav } from './';
@ -88,6 +89,20 @@ describe('AppSearchConfigured', () => {
});
describe('ability checks', () => {
describe('canViewRoleMappings', () => {
it('renders RoleMappings when canViewRoleMappings is true', () => {
setMockValues({ myRole: { canViewRoleMappings: true } });
rerender(wrapper);
expect(wrapper.find(RoleMappingsRouter)).toHaveLength(1);
});
it('does not render RoleMappings when user canViewRoleMappings is false', () => {
setMockValues({ myRole: { canManageEngines: false } });
rerender(wrapper);
expect(wrapper.find(RoleMappingsRouter)).toHaveLength(0);
});
});
describe('canManageEngines', () => {
it('renders EngineCreation when user canManageEngines is true', () => {
setMockValues({ myRole: { canManageEngines: true } });
@ -155,8 +170,6 @@ describe('AppSearchNav', () => {
setMockValues({ myRole: { canViewRoleMappings: true } });
const wrapper = shallow(<AppSearchNav />);
expect(wrapper.find(SideNavLink).last().prop('to')).toEqual(
'http://localhost:3002/as/role_mappings'
);
expect(wrapper.find(SideNavLink).last().prop('to')).toEqual('/role_mappings');
});
});

View file

@ -12,12 +12,13 @@ import { useValues } from 'kea';
import { APP_SEARCH_PLUGIN } from '../../../common/constants';
import { InitialAppData } from '../../../common/types';
import { getAppSearchUrl } from '../shared/enterprise_search_url';
import { HttpLogic } from '../shared/http';
import { KibanaLogic } from '../shared/kibana';
import { Layout, SideNav, SideNavLink } from '../shared/layout';
import { NotFound } from '../shared/not_found';
import { ROLE_MAPPINGS_TITLE } from '../shared/role_mapping/constants';
import { AppLogic } from './app_logic';
import { Credentials, CREDENTIALS_TITLE } from './components/credentials';
import { EngineNav, EngineRouter } from './components/engine';
@ -26,7 +27,7 @@ import { EnginesOverview, ENGINES_TITLE } from './components/engines';
import { ErrorConnecting } from './components/error_connecting';
import { Library } from './components/library';
import { MetaEngineCreation } from './components/meta_engine_creation';
import { ROLE_MAPPINGS_TITLE } from './components/role_mappings';
import { RoleMappingsRouter } from './components/role_mappings';
import { Settings, SETTINGS_TITLE } from './components/settings';
import { SetupGuide } from './components/setup_guide';
import {
@ -64,7 +65,7 @@ export const AppSearchUnconfigured: React.FC = () => (
export const AppSearchConfigured: React.FC<Required<InitialAppData>> = (props) => {
const {
myRole: { canManageEngines, canManageMetaEngines },
myRole: { canManageEngines, canManageMetaEngines, canViewRoleMappings },
} = useValues(AppLogic(props));
const { errorConnecting, readOnlyMode } = useValues(HttpLogic);
@ -101,6 +102,11 @@ export const AppSearchConfigured: React.FC<Required<InitialAppData>> = (props) =
<Route exact path={CREDENTIALS_PATH}>
<Credentials />
</Route>
{canViewRoleMappings && (
<Route path={ROLE_MAPPINGS_PATH}>
<RoleMappingsRouter />
</Route>
)}
{canManageEngines && (
<Route exact path={ENGINE_CREATION_PATH}>
<EngineCreation />
@ -141,7 +147,7 @@ export const AppSearchNav: React.FC<AppSearchNavProps> = ({ subNav }) => {
<SideNavLink to={CREDENTIALS_PATH}>{CREDENTIALS_TITLE}</SideNavLink>
)}
{canViewRoleMappings && (
<SideNavLink isExternal to={getAppSearchUrl(ROLE_MAPPINGS_PATH)}>
<SideNavLink shouldShowActiveForSubroutes to={ROLE_MAPPINGS_PATH}>
{ROLE_MAPPINGS_TITLE}
</SideNavLink>
)}

View file

@ -6,5 +6,5 @@
*/
export * from '../../../common/types/app_search';
export { Role, RoleTypes, AbilityTypes, ASRoleMapping } from './utils/role';
export { Role, RoleTypes, AbilityTypes, ASRoleMapping, AdvanceRoleType } from './utils/role';
export { Engine } from './components/engine/types';

View file

@ -53,3 +53,8 @@ export interface ASRoleMapping extends RoleMapping {
content: string;
};
}
export interface AdvanceRoleType {
type: RoleTypes;
description: string;
}

View file

@ -128,12 +128,7 @@ describe('role mappings routes', () => {
describe('validates', () => {
it('correctly', () => {
const request = {
body: {
...roleMappingBaseSchema,
id: '123',
},
};
const request = { body: roleMappingBaseSchema };
mockRouter.shouldValidate(request);
});

View file

@ -66,10 +66,7 @@ export function registerRoleMappingRoute({
{
path: '/api/app_search/role_mappings/{id}',
validate: {
body: schema.object({
...roleMappingBaseSchema,
id: schema.string(),
}),
body: schema.object(roleMappingBaseSchema),
params: schema.object({
id: schema.string(),
}),

View file

@ -7609,7 +7609,6 @@
"xpack.enterpriseSearch.appSearch.result.documentDetailLink": "ドキュメントの詳細を表示",
"xpack.enterpriseSearch.appSearch.result.hideAdditionalFields": "追加フィールドを非表示",
"xpack.enterpriseSearch.appSearch.result.title": "ドキュメント{id}",
"xpack.enterpriseSearch.appSearch.roleMappings.title": "ロールマッピング",
"xpack.enterpriseSearch.appSearch.settings.logRetention.analytics.label": "分析ログ",
"xpack.enterpriseSearch.appSearch.settings.logRetention.api.label": "API ログ",
"xpack.enterpriseSearch.appSearch.settings.logRetention.description": "API ログと分析のデフォルト書き込み設定を管理します。",

View file

@ -7676,7 +7676,6 @@
"xpack.enterpriseSearch.appSearch.result.hideAdditionalFields": "隐藏其他字段",
"xpack.enterpriseSearch.appSearch.result.showAdditionalFields": "显示其他 {numberOfAdditionalFields, number} 个{numberOfAdditionalFields, plural, other {字段}}",
"xpack.enterpriseSearch.appSearch.result.title": "文档 {id}",
"xpack.enterpriseSearch.appSearch.roleMappings.title": "角色映射",
"xpack.enterpriseSearch.appSearch.settings.logRetention.analytics.label": "分析日志",
"xpack.enterpriseSearch.appSearch.settings.logRetention.api.label": "API 日志",
"xpack.enterpriseSearch.appSearch.settings.logRetention.description": "管理 API 日志和分析的默认写入设置。",