Skip to content

Commit dbde7ef

Browse files
mpolotsk-akamaicorya-akamaibnussman-akamaibnussman
authored
feat: [UIE-8840, UIE-8841, UIE-8842, UIE-8843, UIE-8844] - IAM RBAC: account settings permissions check (#12630)
* feat: [UIE-8840], [UIE-8841], [UIE-8842], [UIE-8843], [UIE-8844] - account settings permissions check * Added changeset: IAM RBAC: add a permission check in Account Settings Tab * fix * e2e tests fix * after review fix * refactor: replace generic hasPermission prop with explicit usePermissions hook * e2e test fix * refactor: use update_account_settings permission instead of enable_linode_backups * update changelog directly --------- Co-authored-by: Conal Ryan <136115382+corya-akamai@users.noreply.github.com> Co-authored-by: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com> Co-authored-by: Banks Nussman <banks@nussman.us>
1 parent dcbc918 commit dbde7ef

20 files changed

+300
-17
lines changed

packages/manager/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
7070
- DataStream: add destination's details for selected destination ([#12559](https://github.com/linode/manager/pull/12559))
7171
- IAM RBAC: Modify query parameter to allow varying use cases, return API errors, and return isLoading and isError values ([#12560](https://github.com/linode/manager/pull/12560))
7272
- IAM RBAC: add a permission check in Profile and Account/Billing ([#12561](https://github.com/linode/manager/pull/12561))
73+
- IAM RBAC: add a permission check in Account Settings Tab ([#12630](https://github.com/linode/manager/pull/12630))
7374
- Add subnet IPv6 to VPC create page ([#12563](https://github.com/linode/manager/pull/12563))
7475
- Add/update inline docs for ACLP Alerts logic ([#12578](https://github.com/linode/manager/pull/12578))
7576
- ACLP: add checkbox functionality in `AlertRegions`. ([#12582](https://github.com/linode/manager/pull/12582))

packages/manager/src/components/MaintenancePolicySelect/MaintenancePolicySelect.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,9 @@ export const MaintenancePolicySelect = (props: Props) => {
109109
</InputAdornment>
110110
),
111111
},
112-
tooltipText: (
112+
tooltipText: disabled ? (
113+
"You don't have permission to change this setting."
114+
) : (
113115
<Stack spacing={2}>
114116
<Typography>
115117
<strong>Migrate:</strong> {MIGRATE_TOOLTIP_TEXT}

packages/manager/src/features/Account/AutoBackups.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { makeStyles } from 'tss-react/mui';
1111

1212
import { Link } from 'src/components/Link';
1313

14+
import { usePermissions } from '../IAM/hooks/usePermissions';
15+
1416
import type { Theme } from '@mui/material/styles';
1517

1618
const useStyles = makeStyles()((theme: Theme) => ({
@@ -47,7 +49,9 @@ const AutoBackups = (props: Props) => {
4749
} = props;
4850

4951
const { classes } = useStyles();
50-
52+
const { data: permissions } = usePermissions('account', [
53+
'update_account_settings',
54+
]);
5155
return (
5256
<Paper>
5357
<Typography variant="h2">Backup Auto Enrollment</Typography>
@@ -77,7 +81,9 @@ const AutoBackups = (props: Props) => {
7781
<Toggle
7882
checked={isManagedCustomer ? true : backups_enabled}
7983
data-qa-toggle-auto-backup
80-
disabled={!!isManagedCustomer}
84+
disabled={
85+
!!isManagedCustomer || !permissions.update_account_settings
86+
}
8187
onChange={onChange}
8288
/>
8389
}

packages/manager/src/features/Account/CloseAccountSetting.test.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,14 @@ import {
1414
// Mock the useProfile hook to immediately return the expected data, circumventing the HTTP request and loading state.
1515
const queryMocks = vi.hoisted(() => ({
1616
useProfile: vi.fn().mockReturnValue({}),
17+
userPermissions: vi.fn(() => ({
18+
data: { cancel_account: true },
19+
})),
1720
}));
1821

22+
vi.mock('src/features/IAM/hooks/usePermissions', () => ({
23+
usePermissions: queryMocks.userPermissions,
24+
}));
1925
vi.mock('@linode/queries', async () => {
2026
const actual = await vi.importActual('@linode/queries');
2127
return {
@@ -104,4 +110,18 @@ describe('Close Account Settings', () => {
104110
expect(button).not.toHaveAttribute('disabled');
105111
expect(button).toHaveAttribute('aria-disabled', 'true');
106112
});
113+
114+
it('should disable Close Account button if the user does not have close_account permissions', async () => {
115+
queryMocks.userPermissions.mockReturnValue({
116+
data: { cancel_account: false },
117+
});
118+
queryMocks.useProfile.mockReturnValue({
119+
data: profileFactory.build({ user_type: 'default' }),
120+
});
121+
122+
const { getByTestId } = renderWithTheme(<CloseAccountSetting />);
123+
const button = getByTestId('close-account-button');
124+
expect(button).toBeInTheDocument();
125+
expect(button).toBeDisabled();
126+
});
107127
});

packages/manager/src/features/Account/CloseAccountSetting.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,21 @@ import { useProfile } from '@linode/queries';
22
import { Box, Button, Paper, Typography } from '@linode/ui';
33
import * as React from 'react';
44

5+
import { usePermissions } from '../IAM/hooks/usePermissions';
56
import CloseAccountDialog from './CloseAccountDialog';
67
import {
78
CHILD_USER_CLOSE_ACCOUNT_TOOLTIP_TEXT,
89
PARENT_USER_CLOSE_ACCOUNT_TOOLTIP_TEXT,
910
PROXY_USER_CLOSE_ACCOUNT_TOOLTIP_TEXT,
1011
} from './constants';
1112

12-
const CloseAccountSetting = () => {
13+
export const CloseAccountSetting = () => {
1314
const [dialogOpen, setDialogOpen] = React.useState<boolean>(false);
1415

1516
const { data: profile } = useProfile();
1617

18+
const { data: permissions } = usePermissions('account', ['cancel_account']);
19+
1720
// Disable the Close Account button for users with a Parent/Proxy/Child user type.
1821
const isCloseAccountDisabled = Boolean(profile?.user_type !== 'default');
1922

@@ -40,9 +43,13 @@ const CloseAccountSetting = () => {
4043
<Button
4144
buttonType="outlined"
4245
data-testid="close-account-button"
43-
disabled={isCloseAccountDisabled}
46+
disabled={isCloseAccountDisabled || !permissions.cancel_account}
4447
onClick={() => setDialogOpen(true)}
45-
tooltipText={closeAccountButtonTooltipText}
48+
tooltipText={
49+
!permissions.cancel_account
50+
? "You don't have permission to close this account."
51+
: closeAccountButtonTooltipText
52+
}
4653
>
4754
Close Account
4855
</Button>

packages/manager/src/features/Account/DefaultFirewalls.test.tsx

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,17 @@ import { renderWithTheme } from 'src/utilities/testHelpers';
1111

1212
import { DefaultFirewalls } from './DefaultFirewalls';
1313

14+
const queryMocks = vi.hoisted(() => ({
15+
useProfile: vi.fn().mockReturnValue({}),
16+
userPermissions: vi.fn(() => ({
17+
data: { update_account_settings: true },
18+
})),
19+
}));
20+
21+
vi.mock('src/features/IAM/hooks/usePermissions', () => ({
22+
usePermissions: queryMocks.userPermissions,
23+
}));
24+
1425
describe('NetworkInterfaces', () => {
1526
it('renders the NetworkInterfaces section', async () => {
1627
const account = accountFactory.build({
@@ -50,4 +61,50 @@ describe('NetworkInterfaces', () => {
5061
expect(getByText('NodeBalancers Firewall')).toBeVisible();
5162
expect(getByText('Save')).toBeVisible();
5263
});
64+
65+
it('should disable Save button and all select boxes if the user does not have "update_account_settings" permissions', async () => {
66+
queryMocks.userPermissions.mockReturnValue({
67+
data: { update_account_settings: false },
68+
});
69+
const account = accountFactory.build({
70+
capabilities: ['Linode Interfaces'],
71+
});
72+
73+
server.use(
74+
http.get('*/v4/account', () => HttpResponse.json(account)),
75+
http.get('*/v4beta/networking/firewalls/settings', () =>
76+
HttpResponse.json(firewallSettingsFactory.build())
77+
),
78+
http.get('*/v4beta/networking/firewalls', () =>
79+
HttpResponse.json(makeResourcePage(firewallFactory.buildList(1)))
80+
)
81+
);
82+
83+
const { getByLabelText, getByText } = renderWithTheme(
84+
<DefaultFirewalls />,
85+
{
86+
flags: { linodeInterfaces: { enabled: true } },
87+
}
88+
);
89+
90+
const configurationSelect = getByLabelText(
91+
'Configuration Profile Interfaces Firewall'
92+
);
93+
expect(configurationSelect).toHaveAttribute('disabled');
94+
95+
const linodePublicSelect = getByLabelText(
96+
'Linode Interfaces - Public Interface Firewall'
97+
);
98+
expect(linodePublicSelect).toHaveAttribute('disabled');
99+
100+
const linodeVPCSelect = getByLabelText(
101+
'Linode Interfaces - VPC Interface Firewall'
102+
);
103+
expect(linodeVPCSelect).toHaveAttribute('disabled');
104+
105+
const nodeBalancerSelect = getByLabelText('NodeBalancers Firewall');
106+
expect(nodeBalancerSelect).toHaveAttribute('disabled');
107+
108+
expect(getByText('Save')).toHaveAttribute('aria-disabled', 'true');
109+
});
53110
});

packages/manager/src/features/Account/DefaultFirewalls.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { Controller, useForm } from 'react-hook-form';
2222
import { useIsLinodeInterfacesEnabled } from 'src/utilities/linodes';
2323

2424
import { FirewallSelect } from '../Firewalls/components/FirewallSelect';
25+
import { usePermissions } from '../IAM/hooks/usePermissions';
2526

2627
import type { UpdateFirewallSettings } from '@linode/api-v4';
2728

@@ -38,7 +39,9 @@ export const DefaultFirewalls = () => {
3839
} = useFirewallSettingsQuery({ enabled: isLinodeInterfacesEnabled });
3940

4041
const { mutateAsync: updateFirewallSettings } = useMutateFirewallSettings();
41-
42+
const { data: permissions } = usePermissions('account', [
43+
'update_account_settings',
44+
]);
4245
const values = {
4346
default_firewall_ids: { ...firewallSettings?.default_firewall_ids },
4447
};
@@ -117,6 +120,7 @@ export const DefaultFirewalls = () => {
117120
render={({ field, fieldState }) => (
118121
<FirewallSelect
119122
disableClearable
123+
disabled={!permissions.update_account_settings}
120124
errorText={fieldState.error?.message}
121125
hideDefaultChips
122126
label="Configuration Profile Interfaces Firewall"
@@ -132,6 +136,7 @@ export const DefaultFirewalls = () => {
132136
render={({ field, fieldState }) => (
133137
<FirewallSelect
134138
disableClearable
139+
disabled={!permissions.update_account_settings}
135140
errorText={fieldState.error?.message}
136141
hideDefaultChips
137142
label="Linode Interfaces - Public Interface Firewall"
@@ -147,6 +152,7 @@ export const DefaultFirewalls = () => {
147152
render={({ field, fieldState }) => (
148153
<FirewallSelect
149154
disableClearable
155+
disabled={!permissions.update_account_settings}
150156
errorText={fieldState.error?.message}
151157
hideDefaultChips
152158
label="Linode Interfaces - VPC Interface Firewall"
@@ -165,6 +171,7 @@ export const DefaultFirewalls = () => {
165171
render={({ field, fieldState }) => (
166172
<FirewallSelect
167173
disableClearable
174+
disabled={!permissions.update_account_settings}
168175
errorText={fieldState.error?.message}
169176
hideDefaultChips
170177
label="NodeBalancers Firewall"
@@ -179,7 +186,7 @@ export const DefaultFirewalls = () => {
179186
<Box sx={(theme) => ({ marginTop: theme.spacingFunction(16) })}>
180187
<Button
181188
buttonType="outlined"
182-
disabled={!isDirty}
189+
disabled={!isDirty || !permissions.update_account_settings}
183190
loading={isSubmitting}
184191
type="submit"
185192
>

packages/manager/src/features/Account/EnableManaged.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import { ConfirmationDialog } from 'src/components/ConfirmationDialog/Confirmati
1616
import { Link } from 'src/components/Link';
1717
import { SupportLink } from 'src/components/SupportLink';
1818

19+
import { usePermissions } from '../IAM/hooks/usePermissions';
20+
1921
import type { APIError } from '@linode/api-v4/lib/types';
2022

2123
interface Props {
@@ -30,6 +32,7 @@ interface ContentProps {
3032
export const ManagedContent = (props: ContentProps) => {
3133
const { isManaged, openConfirmationModal } = props;
3234

35+
const { data: permissions } = usePermissions('account', ['enable_managed']);
3336
if (isManaged) {
3437
return (
3538
<Typography>
@@ -49,7 +52,11 @@ export const ManagedContent = (props: ContentProps) => {
4952
<Link to="https://linode.com/managed">Learn more</Link>.
5053
</Typography>
5154
<Box>
52-
<Button buttonType="outlined" onClick={openConfirmationModal}>
55+
<Button
56+
buttonType="outlined"
57+
disabled={!permissions.enable_managed}
58+
onClick={openConfirmationModal}
59+
>
5360
Add Linode Managed
5461
</Button>
5562
</Box>
@@ -65,6 +72,7 @@ export const EnableManaged = (props: Props) => {
6572
const [error, setError] = React.useState<string | undefined>();
6673
const [isLoading, setLoading] = React.useState<boolean>(false);
6774

75+
const { data: permissions } = usePermissions('account', ['enable_managed']);
6876
const linodeCount = linodes?.results ?? 0;
6977

7078
const handleClose = () => {
@@ -94,6 +102,7 @@ export const EnableManaged = (props: Props) => {
94102
'data-testid': 'submit-managed-enrollment',
95103
label: 'Add Linode Managed',
96104
loading: isLoading,
105+
disabled: !permissions.enable_managed,
97106
onClick: handleSubmit,
98107
}}
99108
secondaryButtonProps={{

packages/manager/src/features/Account/MaintenancePolicy.test.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@ import { renderWithTheme } from 'src/utilities/testHelpers';
99

1010
import { MaintenancePolicy } from './MaintenancePolicy';
1111

12+
const queryMocks = vi.hoisted(() => ({
13+
useProfile: vi.fn().mockReturnValue({}),
14+
userPermissions: vi.fn(() => ({
15+
data: { update_account_settings: true },
16+
})),
17+
}));
18+
19+
vi.mock('src/features/IAM/hooks/usePermissions', () => ({
20+
usePermissions: queryMocks.userPermissions,
21+
}));
1222
describe('MaintenancePolicy', () => {
1323
it('renders the MaintenancePolicy section', () => {
1424
const { getByText } = renderWithTheme(<MaintenancePolicy />);
@@ -50,4 +60,19 @@ describe('MaintenancePolicy', () => {
5060
);
5161
});
5262
});
63+
64+
it('should disable "Save Maintenance Policy" button and the selectbox if the user does not have "update_account_settings" permission', () => {
65+
queryMocks.userPermissions.mockReturnValue({
66+
data: { update_account_settings: false },
67+
});
68+
const { getByText, getByLabelText } = renderWithTheme(
69+
<MaintenancePolicy />
70+
);
71+
72+
expect(getByLabelText('Maintenance Policy')).toHaveAttribute('disabled');
73+
expect(getByText('Save Maintenance Policy')).toHaveAttribute(
74+
'aria-disabled',
75+
'true'
76+
);
77+
});
5378
});

packages/manager/src/features/Account/MaintenancePolicy.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import { MaintenancePolicySelect } from 'src/components/MaintenancePolicySelect/
2222
import { useFlags } from 'src/hooks/useFlags';
2323
import { useUpcomingMaintenanceNotice } from 'src/hooks/useUpcomingMaintenanceNotice';
2424

25+
import { usePermissions } from '../IAM/hooks/usePermissions';
26+
2527
import type { MaintenancePolicyValues } from 'src/hooks/useUpcomingMaintenanceNotice.ts';
2628

2729
export const MaintenancePolicy = () => {
@@ -31,7 +33,9 @@ export const MaintenancePolicy = () => {
3133
const { mutateAsync: updateAccountSettings } = useMutateAccountSettings();
3234

3335
const flags = useFlags();
34-
36+
const { data: permissions } = usePermissions('account', [
37+
'update_account_settings',
38+
]);
3539
const {
3640
control,
3741
formState: { isDirty, isSubmitting },
@@ -90,6 +94,7 @@ export const MaintenancePolicy = () => {
9094
name="maintenance_policy"
9195
render={({ field, fieldState }) => (
9296
<MaintenancePolicySelect
97+
disabled={!permissions.update_account_settings}
9398
errorText={fieldState.error?.message}
9499
hideDefaultChip
95100
onChange={(policy) => field.onChange(policy.slug)}
@@ -100,7 +105,7 @@ export const MaintenancePolicy = () => {
100105
<Box marginTop={2}>
101106
<Button
102107
buttonType="outlined"
103-
disabled={!isDirty}
108+
disabled={!isDirty || !permissions.update_account_settings}
104109
loading={isSubmitting}
105110
type="submit"
106111
>

0 commit comments

Comments
 (0)