mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
d797b0c6b7
commit
cd06251fc4
6 changed files with 202 additions and 64 deletions
|
@ -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 (
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue