Skip to content

Commit c2153e9

Browse files
aaleksee-akamaicpathipacorya-akamai
authored
feat: [UIE-8910] - IAM RBAC: add the missing permission checks for linode (linode#12548)
* feat: [UIE-8910] - IAM RBAC: add the missing permission checks for linode * Added changeset: Add the missing permission checks for linode, update the tooltip * remove tests * address feedback --------- Co-authored-by: cpathipa <119517080+cpathipa@users.noreply.github.com> Co-authored-by: Conal Ryan <136115382+corya-akamai@users.noreply.github.com>
1 parent 335782c commit c2153e9

File tree

9 files changed

+149
-50
lines changed

9 files changed

+149
-50
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Upcoming Features
3+
---
4+
5+
Add the missing permission checks for linode, update the tooltip ([#12548](https://github.com/linode/manager/pull/12548))

packages/manager/src/features/Linodes/LinodeEntityDetail.test.tsx

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -361,36 +361,6 @@ describe('Linode Entity Detail', () => {
361361

362362
expect(encryptionStatusFragment).toBeInTheDocument();
363363
});
364-
365-
it('should disable "Add A Tag" button if the user does not have update_linode permission', async () => {
366-
queryMocks.userPermissions.mockReturnValue({
367-
permissions: {
368-
update_linode: false,
369-
},
370-
});
371-
372-
const { getByText } = renderWithTheme(
373-
<LinodeEntityDetail handlers={handlers} id={5} linode={linode} />
374-
);
375-
const addTagBtn = getByText('Add a tag');
376-
expect(addTagBtn).toBeInTheDocument();
377-
expect(addTagBtn).toBeDisabled();
378-
});
379-
380-
it('should enable "Add A Tag" button if the user has update_linode permission', async () => {
381-
queryMocks.userPermissions.mockReturnValue({
382-
permissions: {
383-
update_linode: true,
384-
},
385-
});
386-
387-
const { getByText } = renderWithTheme(
388-
<LinodeEntityDetail handlers={handlers} id={5} linode={linode} />
389-
);
390-
const addTagBtn = getByText('Add a tag');
391-
expect(addTagBtn).toBeInTheDocument();
392-
expect(addTagBtn).toBeEnabled();
393-
});
394364
});
395365

396366
describe('getSubnetsString function', () => {

packages/manager/src/features/Linodes/LinodeEntityDetail.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,6 @@ export const LinodeEntityDetail = (props: Props) => {
149149
}
150150
footer={
151151
<LinodeEntityDetailFooter
152-
isLinodesGrantReadOnly={!permissions.update_linode}
153152
linodeCreated={linode.created}
154153
linodeId={linode.id}
155154
linodeLabel={linode.label}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import * as React from 'react';
2+
3+
import { renderWithTheme } from 'src/utilities/testHelpers';
4+
5+
import { LinodeEntityDetailFooter } from './LinodeEntityDetailFooter';
6+
7+
const props = {
8+
linodeCreated: '2018-11-01T00:00:00',
9+
linodeId: 1,
10+
linodeLabel: 'test-linode',
11+
linodePlan: 'Linode 4GB',
12+
linodeRegionDisplay: 'us-east',
13+
linodeTags: ['test', 'linode'],
14+
};
15+
16+
const queryMocks = vi.hoisted(() => ({
17+
userPermissions: vi.fn(() => ({
18+
permissions: { update_account: false },
19+
})),
20+
}));
21+
22+
vi.mock('src/features/IAM/hooks/usePermissions', () => ({
23+
usePermissions: queryMocks.userPermissions,
24+
}));
25+
26+
describe('LinodeEntityDetailFooter', () => {
27+
it('should disable "Add a tag" button if the user does not have update_account permission', async () => {
28+
const { getByRole } = renderWithTheme(
29+
<LinodeEntityDetailFooter {...props} />
30+
);
31+
32+
const addTagBtn = getByRole('button', {
33+
name: 'Add a tag',
34+
});
35+
expect(addTagBtn).toHaveAttribute('aria-disabled', 'true');
36+
});
37+
38+
it('should enable "Add a tag" button if the user has update_account permission', async () => {
39+
queryMocks.userPermissions.mockReturnValue({
40+
permissions: { update_account: true },
41+
});
42+
43+
const { getByRole } = renderWithTheme(
44+
<LinodeEntityDetailFooter {...props} />
45+
);
46+
47+
const addTagBtn = getByRole('button', {
48+
name: 'Add a tag',
49+
});
50+
expect(addTagBtn).not.toHaveAttribute('aria-disabled', 'true');
51+
});
52+
});

packages/manager/src/features/Linodes/LinodeEntityDetailFooter.tsx

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ import { useSnackbar } from 'notistack';
55
import * as React from 'react';
66

77
import { TagCell } from 'src/components/TagCell/TagCell';
8-
import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck';
98
import { getAPIErrorOrDefault } from 'src/utilities/errorUtils';
109
import { formatDate } from 'src/utilities/formatDate';
1110

11+
import { usePermissions } from '../IAM/hooks/usePermissions';
1212
import {
1313
StyledBox,
1414
StyledLabelBox,
@@ -18,7 +18,6 @@ import {
1818
} from './LinodeEntityDetail.styles';
1919

2020
interface FooterProps {
21-
isLinodesGrantReadOnly: boolean;
2221
linodeCreated: string;
2322
linodeId: number;
2423
linodeLabel: string;
@@ -33,7 +32,6 @@ export const LinodeEntityDetailFooter = React.memo((props: FooterProps) => {
3332
const { data: profile } = useProfile();
3433

3534
const {
36-
isLinodesGrantReadOnly,
3735
linodeCreated,
3836
linodeId,
3937
linodeLabel,
@@ -42,10 +40,7 @@ export const LinodeEntityDetailFooter = React.memo((props: FooterProps) => {
4240
linodeTags,
4341
} = props;
4442

45-
const isReadOnlyAccountAccess = useRestrictedGlobalGrantCheck({
46-
globalGrantType: 'account_access',
47-
permittedGrantLevel: 'read_write',
48-
});
43+
const { permissions } = usePermissions('account', ['update_account']);
4944

5045
const { mutateAsync: updateLinode } = useLinodeUpdateMutation(linodeId);
5146

@@ -148,7 +143,7 @@ export const LinodeEntityDetailFooter = React.memo((props: FooterProps) => {
148143
}}
149144
>
150145
<TagCell
151-
disabled={isLinodesGrantReadOnly || isReadOnlyAccountAccess}
146+
disabled={!permissions.update_account}
152147
entityLabel={linodeLabel}
153148
sx={{
154149
width: '100%',

packages/manager/src/features/Linodes/LinodesLanding/LinodeActionMenu/LinodeActionMenu.test.tsx

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { linodeBackupsFactory, regionFactory } from '@linode/utilities';
2-
import { screen } from '@testing-library/react';
2+
import { screen, within } from '@testing-library/react';
33
import userEvent from '@testing-library/user-event';
44
import * as React from 'react';
55

@@ -104,6 +104,28 @@ describe('LinodeActionMenu', () => {
104104
expect(queryByText('Power Off')).toBeNull();
105105
});
106106

107+
it('should disable Power On when the Linode is rebooting', async () => {
108+
const { getByLabelText, queryByTestId } = renderWithTheme(
109+
<LinodeActionMenu {...props} linodeStatus="rebooting" />
110+
);
111+
112+
const actionMenuButton = getByLabelText(
113+
`Action menu for Linode ${props.linodeLabel}`
114+
);
115+
116+
await userEvent.click(actionMenuButton);
117+
118+
const powerOnItem = queryByTestId('Power On');
119+
expect(powerOnItem).toHaveAttribute('aria-disabled', 'true');
120+
121+
const tooltipButton = within(powerOnItem!).getByRole('button');
122+
123+
expect(tooltipButton).toHaveAttribute(
124+
'aria-label',
125+
'This action is unavailable while your Linode is offline.'
126+
);
127+
});
128+
107129
it('should allow a reboot if the Linode is running', async () => {
108130
renderWithTheme(<LinodeActionMenu {...props} />);
109131
await userEvent.click(screen.getByLabelText(/^Action menu for/));
@@ -114,7 +136,7 @@ describe('LinodeActionMenu', () => {
114136
// TODO: Should check for "read_only" permissions too
115137
renderWithTheme(<LinodeActionMenu {...props} linodeStatus="offline" />);
116138
await userEvent.click(screen.getByLabelText(/^Action menu for/));
117-
expect(screen.queryByText('Reboot')?.closest('li')).toHaveAttribute(
139+
expect(screen.queryByTestId('Reboot')).toHaveAttribute(
118140
'aria-disabled',
119141
'true'
120142
);
@@ -223,7 +245,7 @@ describe('LinodeActionMenu', () => {
223245

224246
for (const action of actions) {
225247
expect(getByText(action)).toBeVisible();
226-
expect(screen.queryByText(action)?.closest('li')).toHaveAttribute(
248+
expect(screen.queryByTestId(action)).toHaveAttribute(
227249
'aria-disabled',
228250
'true'
229251
);

packages/manager/src/features/Linodes/LinodesLanding/LinodeActionMenu/LinodeActionMenu.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,11 @@ export const LinodeActionMenu = (props: LinodeActionMenuProps) => {
8888
const isMTCLinode = Boolean(linodeType && isMTCPlan(linodeType));
8989
const isLinodeRunning = linodeStatus === 'running';
9090

91+
const isStatusNotEligible = !['offline', 'running'].includes(linodeStatus);
92+
const lacksBootPermission = !isLinodeRunning && !permissions.boot_linode;
93+
const lacksShutdownPermission =
94+
isLinodeRunning && !permissions.shutdown_linode;
95+
9196
const actionConfigs: ActionConfig[] = [
9297
{
9398
condition: true,
@@ -99,9 +104,11 @@ export const LinodeActionMenu = (props: LinodeActionMenuProps) => {
99104
onClick: handlePowerAction,
100105
title: isLinodeRunning ? 'Power Off' : 'Power On',
101106
tooltipAction: 'modify',
102-
tooltipText: !permissions.shutdown_linode
103-
? NO_PERMISSION_TOOLTIP_TEXT
104-
: undefined,
107+
tooltipText: isStatusNotEligible
108+
? LINODE_STATUS_NOT_RUNNING_TOOLTIP_TEXT
109+
: lacksBootPermission || lacksShutdownPermission
110+
? NO_PERMISSION_TOOLTIP_TEXT
111+
: undefined,
105112
},
106113
{
107114
condition: true,
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import React from 'react';
2+
3+
import { renderWithTheme } from 'src/utilities/testHelpers';
4+
5+
import { LinodesLandingEmptyState } from './LinodesLandingEmptyState';
6+
7+
const queryMocks = vi.hoisted(() => ({
8+
userPermissions: vi.fn(() => ({
9+
permissions: {
10+
create_linode: false,
11+
},
12+
})),
13+
useNavigate: vi.fn(),
14+
}));
15+
16+
vi.mock('src/features/IAM/hooks/usePermissions', () => ({
17+
usePermissions: queryMocks.userPermissions,
18+
}));
19+
20+
vi.mock('@tanstack/react-router', async () => {
21+
const actual = await vi.importActual('@tanstack/react-router');
22+
return {
23+
...actual,
24+
useNavigate: queryMocks.useNavigate,
25+
};
26+
});
27+
28+
describe('LinodesLandingEmptyState', () => {
29+
it('should disable "Create Linode" button if the user does not have create_linode permission', () => {
30+
const { getByRole } = renderWithTheme(<LinodesLandingEmptyState />);
31+
32+
const createLinodeBtn = getByRole('button', { name: 'Create Linode' });
33+
34+
expect(createLinodeBtn).toBeInTheDocument();
35+
expect(createLinodeBtn).toHaveAttribute('aria-disabled', 'true');
36+
});
37+
38+
it('should enable "Create Linode" button if the user has create_linode permission', () => {
39+
queryMocks.userPermissions.mockReturnValue({
40+
permissions: {
41+
create_linode: true,
42+
},
43+
});
44+
const { getByRole } = renderWithTheme(<LinodesLandingEmptyState />);
45+
46+
const createLinodeBtn = getByRole('button', { name: 'Create Linode' });
47+
48+
expect(createLinodeBtn).toBeInTheDocument();
49+
expect(createLinodeBtn).not.toHaveAttribute('aria-disabled', 'true');
50+
});
51+
});

packages/manager/src/features/Linodes/LinodesLanding/LinodesLandingEmptyState.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { ResourcesLinksSubSection } from 'src/components/EmptyLandingPageResourc
99
import { ResourcesMoreLink } from 'src/components/EmptyLandingPageResources/ResourcesMoreLink';
1010
import { ResourcesSection } from 'src/components/EmptyLandingPageResources/ResourcesSection';
1111
import { getRestrictedResourceText } from 'src/features/Account/utils';
12-
import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck';
12+
import { usePermissions } from 'src/features/IAM/hooks/usePermissions';
1313
import { sendEvent } from 'src/utilities/analytics/utils';
1414
import { getLinkOnClick } from 'src/utilities/emptyStateLandingUtils';
1515

@@ -26,9 +26,7 @@ const APPS_MORE_LINKS_TEXT = 'See all Marketplace apps';
2626
export const LinodesLandingEmptyState = () => {
2727
const navigate = useNavigate();
2828

29-
const isLinodesGrantReadOnly = useRestrictedGlobalGrantCheck({
30-
globalGrantType: 'add_linodes',
31-
});
29+
const { permissions } = usePermissions('account', ['create_linode']);
3230

3331
return (
3432
<React.Fragment>
@@ -37,7 +35,7 @@ export const LinodesLandingEmptyState = () => {
3735
buttonProps={[
3836
{
3937
children: 'Create Linode',
40-
disabled: isLinodesGrantReadOnly,
38+
disabled: !permissions.create_linode,
4139
onClick: () => {
4240
navigate({ to: '/linodes/create' });
4341
sendEvent({

0 commit comments

Comments
 (0)