[APM] Custom links can still be created with a read only user. (#87089)

* disabling buttons when user does not permission

* fixing test

* disabling create/edit button when user does not have write permission

* addressing PR comments

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Cauê Marcondes 2021-01-04 13:50:04 +01:00 committed by GitHub
parent d797b0c6b7
commit cd06251fc4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 202 additions and 64 deletions

View file

@ -41,6 +41,7 @@ interface Props {
export function AgentConfigurationList({ status, data, refetch }: Props) {
const { core } = useApmPluginContext();
const canSave = core.application.capabilities.apm.save;
const { basePath } = core.http;
const { search } = useLocation();
const theme = useTheme();
@ -180,28 +181,36 @@ export function AgentConfigurationList({ status, data, refetch }: Props) {
<TimestampTooltip time={value} timeUnit="minutes" />
),
},
{
width: px(units.double),
name: '',
render: (config: Config) => (
<EuiButtonIcon
aria-label="Edit"
iconType="pencil"
href={editAgentConfigurationHref(config.service, search, basePath)}
/>
),
},
{
width: px(units.double),
name: '',
render: (config: Config) => (
<EuiButtonIcon
aria-label="Delete"
iconType="trash"
onClick={() => setConfigToBeDeleted(config)}
/>
),
},
...(canSave
? [
{
width: px(units.double),
name: '',
render: (config: Config) => (
<EuiButtonIcon
aria-label="Edit"
iconType="pencil"
href={editAgentConfigurationHref(
config.service,
search,
basePath
)}
/>
),
},
{
width: px(units.double),
name: '',
render: (config: Config) => (
<EuiButtonIcon
aria-label="Delete"
iconType="trash"
onClick={() => setConfigToBeDeleted(config)}
/>
),
},
]
: []),
];
return (

View file

@ -3,6 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiToolTip } from '@elastic/eui';
import {
EuiButton,
EuiFlexGroup,
@ -73,15 +74,35 @@ function CreateConfigurationButton() {
const { basePath } = core.http;
const { search } = useLocation();
const href = createAgentConfigurationHref(search, basePath);
const canSave = core.application.capabilities.apm.save;
return (
<EuiFlexItem>
<EuiFlexGroup alignItems="center" justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButton color="primary" fill iconType="plusInCircle" href={href}>
{i18n.translate('xpack.apm.agentConfig.createConfigButtonLabel', {
defaultMessage: 'Create configuration',
})}
</EuiButton>
<EuiToolTip
content={
!canSave &&
i18n.translate(
'xpack.apm.agentConfig.configurationsPanelTitle.noPermissionTooltipLabel',
{
defaultMessage:
"Your user role doesn't have permissions to create agent configurations",
}
)
}
>
<EuiButton
color="primary"
fill
iconType="plusInCircle"
href={href}
isDisabled={!canSave}
>
{i18n.translate('xpack.apm.agentConfig.createConfigButtonLabel', {
defaultMessage: 'Create configuration',
})}
</EuiButton>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>

View file

@ -3,17 +3,40 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { EuiButton } from '@elastic/eui';
import { EuiButton, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { useApmPluginContext } from '../../../../../context/apm_plugin/use_apm_plugin_context';
export function CreateCustomLinkButton({ onClick }: { onClick: () => void }) {
const { core } = useApmPluginContext();
const canSave = core.application.capabilities.apm.save;
return (
<EuiButton color="primary" fill iconType="plusInCircle" onClick={onClick}>
{i18n.translate(
'xpack.apm.settings.customizeUI.customLink.createCustomLink',
{ defaultMessage: 'Create custom link' }
)}
</EuiButton>
<EuiToolTip
content={
!canSave &&
i18n.translate(
'xpack.apm.settings.customizeUI.customLink.noPermissionTooltipLabel',
{
defaultMessage:
"Your user role doesn't have permissions to create custom links",
}
)
}
>
<EuiButton
color="primary"
fill
iconType="plusInCircle"
onClick={onClick}
isDisabled={!canSave}
data-test-subj="createButton"
>
{i18n.translate(
'xpack.apm.settings.customizeUI.customLink.createCustomLink',
{ defaultMessage: 'Create custom link' }
)}
</EuiButton>
</EuiToolTip>
);
}

View file

@ -13,6 +13,7 @@ import {
EuiSpacer,
} from '@elastic/eui';
import { isEmpty } from 'lodash';
import { useApmPluginContext } from '../../../../../context/apm_plugin/use_apm_plugin_context';
import { CustomLink } from '../../../../../../common/custom_link/custom_link_types';
import { units, px } from '../../../../../style/variables';
import { ManagedTable } from '../../../../shared/ManagedTable';
@ -26,6 +27,8 @@ interface Props {
export function CustomLinkTable({ items = [], onCustomLinkSelected }: Props) {
const [searchTerm, setSearchTerm] = useState('');
const { core } = useApmPluginContext();
const canSave = core.application.capabilities.apm.save;
const columns = [
{
@ -61,22 +64,26 @@ export function CustomLinkTable({ items = [], onCustomLinkSelected }: Props) {
width: px(units.triple),
name: '',
actions: [
{
name: i18n.translate(
'xpack.apm.settings.customizeUI.customLink.table.editButtonLabel',
{ defaultMessage: 'Edit' }
),
description: i18n.translate(
'xpack.apm.settings.customizeUI.customLink.table.editButtonDescription',
{ defaultMessage: 'Edit this custom link' }
),
icon: 'pencil',
color: 'primary',
type: 'icon',
onClick: (customLink: CustomLink) => {
onCustomLinkSelected(customLink);
},
},
...(canSave
? [
{
name: i18n.translate(
'xpack.apm.settings.customizeUI.customLink.table.editButtonLabel',
{ defaultMessage: 'Edit' }
),
description: i18n.translate(
'xpack.apm.settings.customizeUI.customLink.table.editButtonDescription',
{ defaultMessage: 'Edit this custom link' }
),
icon: 'pencil',
color: 'primary',
type: 'icon',
onClick: (customLink: CustomLink) => {
onCustomLinkSelected(customLink);
},
},
]
: []),
],
},
];

View file

@ -7,22 +7,26 @@
import {
fireEvent,
render,
waitFor,
RenderResult,
waitFor,
} from '@testing-library/react';
import React from 'react';
import { act } from 'react-dom/test-utils';
import * as apmApi from '../../../../../services/rest/createCallApmApi';
import { License } from '../../../../../../../licensing/common/license';
import * as hooks from '../../../../../hooks/use_fetcher';
import { LicenseContext } from '../../../../../context/license/license_context';
import { CustomLinkOverview } from '.';
import { License } from '../../../../../../../licensing/common/license';
import { ApmPluginContextValue } from '../../../../../context/apm_plugin/apm_plugin_context';
import {
mockApmPluginContextValue,
MockApmPluginContextWrapper,
} from '../../../../../context/apm_plugin/mock_apm_plugin_context';
import { LicenseContext } from '../../../../../context/license/license_context';
import * as hooks from '../../../../../hooks/use_fetcher';
import * as apmApi from '../../../../../services/rest/createCallApmApi';
import {
expectTextsInDocument,
expectTextsNotInDocument,
} from '../../../../../utils/testHelpers';
import * as saveCustomLink from './CreateEditCustomLinkFlyout/saveCustomLink';
import { MockApmPluginContextWrapper } from '../../../../../context/apm_plugin/mock_apm_plugin_context';
const data = [
{
@ -39,6 +43,16 @@ const data = [
},
];
function getMockAPMContext({ canSave }: { canSave: boolean }) {
return ({
...mockApmPluginContextValue,
core: {
...mockApmPluginContextValue.core,
application: { capabilities: { apm: { save: canSave }, ml: {} } },
},
} as unknown) as ApmPluginContextValue;
}
describe('CustomLink', () => {
beforeAll(() => {
jest.spyOn(apmApi, 'callApmApi').mockResolvedValue({});
@ -70,9 +84,11 @@ describe('CustomLink', () => {
});
it('shows when no link is available', () => {
const component = render(
<LicenseContext.Provider value={goldLicense}>
<CustomLinkOverview />
</LicenseContext.Provider>
<MockApmPluginContextWrapper>
<LicenseContext.Provider value={goldLicense}>
<CustomLinkOverview />
</LicenseContext.Provider>
</MockApmPluginContextWrapper>
);
expectTextsInDocument(component, ['No links found.']);
});
@ -91,6 +107,34 @@ describe('CustomLink', () => {
jest.clearAllMocks();
});
it('enables create button when user has writte privileges', () => {
const mockContext = getMockAPMContext({ canSave: true });
const { getByTestId } = render(
<LicenseContext.Provider value={goldLicense}>
<MockApmPluginContextWrapper value={mockContext}>
<CustomLinkOverview />
</MockApmPluginContextWrapper>
</LicenseContext.Provider>
);
const createButton = getByTestId('createButton') as HTMLButtonElement;
expect(createButton.disabled).toBeFalsy();
});
it('enables edit button on custom link table when user has writte privileges', () => {
const mockContext = getMockAPMContext({ canSave: true });
const { getAllByText } = render(
<LicenseContext.Provider value={goldLicense}>
<MockApmPluginContextWrapper value={mockContext}>
<CustomLinkOverview />
</MockApmPluginContextWrapper>
</LicenseContext.Provider>
);
expect(getAllByText('Edit').length).toEqual(2);
});
it('shows a table with all custom link', () => {
const component = render(
<LicenseContext.Provider value={goldLicense}>
@ -108,9 +152,11 @@ describe('CustomLink', () => {
});
it('checks if create custom link button is available and working', () => {
const mockContext = getMockAPMContext({ canSave: true });
const { queryByText, getByText } = render(
<LicenseContext.Provider value={goldLicense}>
<MockApmPluginContextWrapper>
<MockApmPluginContextWrapper value={mockContext}>
<CustomLinkOverview />
</MockApmPluginContextWrapper>
</LicenseContext.Provider>
@ -137,9 +183,10 @@ describe('CustomLink', () => {
});
const openFlyout = () => {
const mockContext = getMockAPMContext({ canSave: true });
const component = render(
<LicenseContext.Provider value={goldLicense}>
<MockApmPluginContextWrapper>
<MockApmPluginContextWrapper value={mockContext}>
<CustomLinkOverview />
</MockApmPluginContextWrapper>
</LicenseContext.Provider>
@ -173,9 +220,10 @@ describe('CustomLink', () => {
});
it('deletes a custom link', async () => {
const mockContext = getMockAPMContext({ canSave: true });
const component = render(
<LicenseContext.Provider value={goldLicense}>
<MockApmPluginContextWrapper>
<MockApmPluginContextWrapper value={mockContext}>
<CustomLinkOverview />
</MockApmPluginContextWrapper>
</LicenseContext.Provider>
@ -356,4 +404,34 @@ describe('CustomLink', () => {
expectTextsNotInDocument(component, ['Start free 30-day trial']);
});
});
describe('with read-only user', () => {
it('disables create custom link button', () => {
const mockContext = getMockAPMContext({ canSave: false });
const { getByTestId } = render(
<LicenseContext.Provider value={goldLicense}>
<MockApmPluginContextWrapper value={mockContext}>
<CustomLinkOverview />
</MockApmPluginContextWrapper>
</LicenseContext.Provider>
);
const createButton = getByTestId('createButton') as HTMLButtonElement;
expect(createButton.disabled).toBeTruthy();
});
it('removes edit button on custom link table', () => {
const mockContext = getMockAPMContext({ canSave: false });
const { queryAllByText } = render(
<LicenseContext.Provider value={goldLicense}>
<MockApmPluginContextWrapper value={mockContext}>
<CustomLinkOverview />
</MockApmPluginContextWrapper>
</LicenseContext.Provider>
);
expect(queryAllByText('Edit').length).toEqual(0);
});
});
});

View file

@ -64,7 +64,7 @@ export const createCustomLinkRoute = createRoute({
params: t.type({
body: payloadRt,
}),
options: { tags: ['access:apm'] },
options: { tags: ['access:apm', 'access:apm_write'] },
handler: async ({ context, request }) => {
if (!isActiveGoldLicense(context.licensing.license)) {
throw Boom.forbidden(INVALID_LICENSE);