Skip to content

Commit 537e075

Browse files
feat: [UIE-9127] - IAM RBAC: VPC Create permissions check (linode#12863)
* IAM RBAC: VPC Create - add permissions check * unit tests * Added changeset: IAM RBAC: Implements IAM RBAC permissions for VPC Create page * IP Stack permissions check
1 parent 4e670d9 commit 537e075

File tree

5 files changed

+76
-11
lines changed

5 files changed

+76
-11
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+
IAM RBAC: Implements IAM RBAC permissions for VPC Create page ([#12863](https://github.com/linode/manager/pull/12863))

packages/manager/src/features/VPCs/VPCCreate/FormComponents/VPCTopSectionContent.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { FormLabel } from 'src/components/FormLabel';
2424
import { Link } from 'src/components/Link';
2525
import { RegionSelect } from 'src/components/RegionSelect/RegionSelect';
2626
import { SelectionCard } from 'src/components/SelectionCard/SelectionCard';
27+
import { usePermissions } from 'src/features/IAM/hooks/usePermissions';
2728
import { useGetLinodeCreateType } from 'src/features/Linodes/LinodeCreate/Tabs/utils/useGetLinodeCreateType';
2829
import { useFlags } from 'src/hooks/useFlags';
2930
import { useVPCDualStack } from 'src/hooks/useVPCDualStack';
@@ -67,6 +68,8 @@ export const VPCTopSectionContent = (props: Props) => {
6768
const subnets = useWatch({ control, name: 'subnets' });
6869
const vpcIPv6 = useWatch({ control, name: 'ipv6' });
6970

71+
const { data: permissions } = usePermissions('account', ['create_vpc']);
72+
7073
const { isDualStackEnabled, isDualStackSelected, isEnterpriseCustomer } =
7174
useVPCDualStack(vpcIPv6);
7275

@@ -150,6 +153,7 @@ export const VPCTopSectionContent = (props: Props) => {
150153
<Grid container spacing={2}>
151154
<SelectionCard
152155
checked={!isDualStackSelected}
156+
disabled={!permissions?.create_vpc}
153157
gridSize={{
154158
md: isDrawer ? 12 : 3,
155159
sm: 12,
@@ -165,7 +169,12 @@ export const VPCTopSectionContent = (props: Props) => {
165169
})
166170
);
167171
}}
168-
renderIcon={() => <Radio checked={!isDualStackSelected} />}
172+
renderIcon={() => (
173+
<Radio
174+
checked={!isDualStackSelected}
175+
disabled={!permissions?.create_vpc}
176+
/>
177+
)}
169178
renderVariant={() => (
170179
<TooltipIcon
171180
status="info"
@@ -189,6 +198,7 @@ export const VPCTopSectionContent = (props: Props) => {
189198
/>
190199
<SelectionCard
191200
checked={isDualStackSelected}
201+
disabled={!permissions?.create_vpc}
192202
gridSize={{
193203
md: isDrawer ? 12 : 3,
194204
sm: 12,
@@ -208,7 +218,12 @@ export const VPCTopSectionContent = (props: Props) => {
208218
})
209219
);
210220
}}
211-
renderIcon={() => <Radio checked={isDualStackSelected} />}
221+
renderIcon={() => (
222+
<Radio
223+
checked={isDualStackSelected}
224+
disabled={!permissions?.create_vpc}
225+
/>
226+
)}
212227
renderVariant={() => (
213228
<TooltipIcon
214229
status="info"
@@ -263,12 +278,14 @@ export const VPCTopSectionContent = (props: Props) => {
263278
<FormControlLabel
264279
checked={vpcIPv6 && vpcIPv6[0].range === '/52'}
265280
control={<Radio />}
281+
disabled={!permissions?.create_vpc}
266282
label="/52"
267283
value="/52"
268284
/>
269285
<FormControlLabel
270286
checked={vpcIPv6 && vpcIPv6[0].range === '/48'}
271287
control={<Radio />}
288+
disabled={!permissions?.create_vpc}
272289
label="/48"
273290
value="/48"
274291
/>

packages/manager/src/features/VPCs/VPCCreate/SubnetNode.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ export const SubnetNode = (props: Props) => {
114114
<Grid size={1}>
115115
<StyledButton
116116
aria-label={`Remove Subnet ${label !== '' ? label : idx}`}
117+
disabled={disabled}
117118
onClick={() => remove(idx)}
118119
>
119120
<CloseIcon data-testid={`delete-subnet-${idx}`} />

packages/manager/src/features/VPCs/VPCCreate/VPCCreate.test.tsx

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

77
import VPCCreate from './VPCCreate';
88

9+
const queryMocks = vi.hoisted(() => ({
10+
userPermissions: vi.fn(() => ({
11+
data: {
12+
create_vpc: true,
13+
},
14+
})),
15+
}));
16+
vi.mock('src/features/IAM/hooks/usePermissions', () => ({
17+
usePermissions: queryMocks.userPermissions,
18+
}));
19+
920
beforeEach(() => {
1021
// ignores the console errors in these tests as they're supposed to happen
1122
vi.spyOn(console, 'error').mockImplementation(() => {});
@@ -74,4 +85,40 @@ describe('VPC create page', () => {
7485
expect(subnetIP[4]).toBeInTheDocument();
7586
expect(subnetIP[4]).toHaveValue('10.0.0.0/24');
7687
});
88+
89+
it('should disable inputs if user does not have create_vpc permission', async () => {
90+
queryMocks.userPermissions.mockReturnValue({
91+
data: {
92+
create_vpc: false,
93+
},
94+
});
95+
const { getByLabelText, getByText } = renderWithTheme(<VPCCreate />);
96+
97+
expect(getByLabelText('Region')).toBeDisabled();
98+
expect(getByLabelText('VPC Label')).toBeDisabled();
99+
const description = screen.getByRole('textbox', { name: /description/i });
100+
expect(description).toBeDisabled();
101+
expect(getByLabelText('Subnet Label')).toBeDisabled();
102+
expect(getByLabelText('Subnet IP Address Range')).toBeDisabled();
103+
expect(getByText('Add another Subnet')).toBeDisabled();
104+
expect(getByText('Create VPC')).toBeDisabled();
105+
});
106+
107+
it('should enable inputs if user has create_vpc permission', async () => {
108+
queryMocks.userPermissions.mockReturnValue({
109+
data: {
110+
create_vpc: true,
111+
},
112+
});
113+
const { getByLabelText, getByText } = renderWithTheme(<VPCCreate />);
114+
115+
expect(getByLabelText('Region')).toBeEnabled();
116+
expect(getByLabelText('VPC Label')).toBeEnabled();
117+
const description = screen.getByRole('textbox', { name: /description/i });
118+
expect(description).toBeEnabled();
119+
expect(getByLabelText('Subnet Label')).toBeEnabled();
120+
expect(getByLabelText('Subnet IP Address Range')).toBeEnabled();
121+
expect(getByText('Add another Subnet')).toBeEnabled();
122+
expect(getByText('Create VPC')).toBeEnabled();
123+
});
77124
});

packages/manager/src/hooks/useCreateVPC.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
11
import { yupResolver } from '@hookform/resolvers/yup';
22
import { isEmpty } from '@linode/api-v4';
3-
import {
4-
useCreateVPCMutation,
5-
useGrants,
6-
useProfile,
7-
useRegionsQuery,
8-
} from '@linode/queries';
3+
import { useCreateVPCMutation, useRegionsQuery } from '@linode/queries';
94
import { scrollErrorIntoView } from '@linode/utilities';
105
import { createVPCSchema } from '@linode/validation';
116
import { useNavigate } from '@tanstack/react-router';
127
import * as React from 'react';
138
import { useForm } from 'react-hook-form';
149

10+
import { usePermissions } from 'src/features/IAM/hooks/usePermissions';
1511
import { useGetLinodeCreateType } from 'src/features/Linodes/LinodeCreate/Tabs/utils/useGetLinodeCreateType';
1612
import { sendLinodeCreateFormStepEvent } from 'src/utilities/analytics/formEventAnalytics';
1713
import { DEFAULT_SUBNET_IPV4_VALUE } from 'src/utilities/subnets';
@@ -34,9 +30,8 @@ export const useCreateVPC = (inputs: UseCreateVPCInputs) => {
3430

3531
const previousSubmitCount = React.useRef<number>(0);
3632

37-
const { data: profile } = useProfile();
38-
const { data: grants } = useGrants();
39-
const userCannotAddVPC = profile?.restricted && !grants?.global.add_vpcs;
33+
const { data: permissions } = usePermissions('account', ['create_vpc']);
34+
const userCannotAddVPC = !permissions?.create_vpc;
4035

4136
const createType = useGetLinodeCreateType();
4237
const isFromLinodeCreate = location.pathname.includes('/linodes/create');

0 commit comments

Comments
 (0)