Skip to content

Commit 19cbc5b

Browse files
[UIE-9148] - IAM / RBAC - Support permission segmentation for LA (linode#12764)
* Do the deed * hook logic * fix and improve test * fix MaintenancePolicy permissions * improve hook logic based on feedback * feedback @hana-akamai * revert API changes * changesets * tests + improved & cleaned up logic
1 parent 9e51c36 commit 19cbc5b

File tree

6 files changed

+148
-15
lines changed

6 files changed

+148
-15
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/api-v4": Fixed
3+
---
4+
5+
Wrong import path for EntityType ([#12764](https://github.com/linode/manager/pull/12764))

packages/api-v4/src/iam/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { EntityType } from 'src/entities/types';
1+
import type { EntityType } from '../entities/types';
22

33
export type AccountType = 'account';
44

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Changed
3+
---
4+
5+
Support IAM/RBAC permission segmentation for BETA/LA features ([#12764](https://github.com/linode/manager/pull/12764))

packages/manager/src/features/IAM/Shared/utilities.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,7 @@ export const mergeAssignedRolesIntoExistingRoles = (
513513
selectedPlusExistingRoles.entity_access.push({
514514
id: e.value,
515515
roles: [r.role?.value as EntityRoleType],
516-
type: r.role?.entity_type,
516+
type: r.role?.entity_type as AccessType,
517517
});
518518
}
519519
});

packages/manager/src/features/IAM/hooks/usePermissions.test.ts

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,22 @@ vi.mock(import('@linode/queries'), async (importOriginal) => {
2323
const actual = await importOriginal();
2424
return {
2525
...actual,
26-
useIsIAMEnabled: queryMocks.useIsIAMEnabled,
2726
useUserAccountPermissions: queryMocks.useUserAccountPermissions,
2827
useUserEntityPermissions: queryMocks.useUserEntityPermissions,
2928
useGrants: queryMocks.useGrants,
3029
};
3130
});
3231

32+
vi.mock('src/features/IAM/hooks/useIsIAMEnabled', async () => {
33+
const actual = await vi.importActual(
34+
'src/features/IAM/hooks/useIsIAMEnabled'
35+
);
36+
return {
37+
...actual,
38+
useIsIAMEnabled: queryMocks.useIsIAMEnabled,
39+
};
40+
});
41+
3342
vi.mock('./adapters', () => ({
3443
fromGrants: vi.fn(
3544
(
@@ -127,4 +136,83 @@ describe('usePermissions', () => {
127136
false
128137
);
129138
});
139+
140+
it('returns correct map when IAM beta is false', () => {
141+
queryMocks.useIsIAMEnabled.mockReturnValue({
142+
isIAMEnabled: true,
143+
isIAMBeta: false,
144+
});
145+
const flags = { iam: { beta: false, enabled: true } };
146+
147+
renderHook(() => usePermissions('account', ['create_linode']), {
148+
wrapper: (ui) => wrapWithTheme(ui, { flags }),
149+
});
150+
151+
expect(queryMocks.useGrants).toHaveBeenCalledWith(false);
152+
expect(queryMocks.useUserEntityPermissions).toHaveBeenCalledWith(
153+
'account',
154+
undefined,
155+
true
156+
);
157+
});
158+
159+
it('returns correct map when beta is true and neither the access type nor the permissions are in the limited availability scope', () => {
160+
const flags = { iam: { beta: true, enabled: true } };
161+
queryMocks.useIsIAMEnabled.mockReturnValue({
162+
isIAMEnabled: true,
163+
isIAMBeta: true,
164+
});
165+
166+
renderHook(() => usePermissions('linode', ['update_linode'], 123), {
167+
wrapper: (ui) => wrapWithTheme(ui, { flags }),
168+
});
169+
170+
expect(queryMocks.useGrants).toHaveBeenCalledWith(false);
171+
expect(queryMocks.useUserAccountPermissions).toHaveBeenCalledWith(false);
172+
expect(queryMocks.useUserEntityPermissions).toHaveBeenCalledWith(
173+
'linode',
174+
123,
175+
true
176+
);
177+
});
178+
179+
it('returns correct map when beta is true and the access type is in the limited availability scope', () => {
180+
const flags = { iam: { beta: true, enabled: true } };
181+
queryMocks.useIsIAMEnabled.mockReturnValue({
182+
isIAMEnabled: true,
183+
isIAMBeta: true,
184+
});
185+
186+
renderHook(() => usePermissions('volume', ['resize_volume'], 123), {
187+
wrapper: (ui) => wrapWithTheme(ui, { flags }),
188+
});
189+
190+
expect(queryMocks.useGrants).toHaveBeenCalledWith(true);
191+
expect(queryMocks.useUserAccountPermissions).toHaveBeenCalledWith(false);
192+
expect(queryMocks.useUserEntityPermissions).toHaveBeenCalledWith(
193+
'volume',
194+
123,
195+
false
196+
);
197+
});
198+
199+
it('returns correct map when beta is true and one of the permissions is in the limited availability scope', () => {
200+
const flags = { iam: { beta: true, enabled: true } };
201+
queryMocks.useIsIAMEnabled.mockReturnValue({
202+
isIAMEnabled: true,
203+
isIAMBeta: true,
204+
});
205+
206+
renderHook(() => usePermissions('account', ['create_volume']), {
207+
wrapper: (ui) => wrapWithTheme(ui, { flags }),
208+
});
209+
210+
expect(queryMocks.useGrants).toHaveBeenCalledWith(true);
211+
expect(queryMocks.useUserAccountPermissions).toHaveBeenCalledWith(false);
212+
expect(queryMocks.useUserEntityPermissions).toHaveBeenCalledWith(
213+
'account',
214+
undefined,
215+
false
216+
);
217+
});
130218
});

packages/manager/src/features/IAM/hooks/usePermissions.ts

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
import {
2-
type AccessType,
3-
getUserEntityPermissions,
4-
type PermissionType,
5-
} from '@linode/api-v4';
1+
import { getUserEntityPermissions } from '@linode/api-v4';
62
import {
73
useGrants,
84
useProfile,
@@ -20,14 +16,26 @@ import {
2016
import { useIsIAMEnabled } from './useIsIAMEnabled';
2117

2218
import type {
19+
AccessType,
20+
AccountAdmin,
2321
AccountEntity,
2422
APIError,
2523
EntityType,
2624
GrantType,
25+
PermissionType,
2726
Profile,
2827
} from '@linode/api-v4';
2928
import type { UseQueryResult } from '@linode/queries';
3029

30+
const BETA_ACCESS_TYPE_SCOPE: AccessType[] = ['account', 'linode', 'firewall'];
31+
const LA_ACCOUNT_ADMIN_PERMISSIONS_TO_EXCLUDE = [
32+
'create_image',
33+
'upload_image',
34+
'create_vpc',
35+
'create_volume',
36+
'create_nodebalancer',
37+
];
38+
3139
export type PermissionsResult<T extends readonly PermissionType[]> = {
3240
data: Record<T[number], boolean>;
3341
} & Omit<UseQueryResult<PermissionType[], APIError[]>, 'data'>;
@@ -38,23 +46,50 @@ export const usePermissions = <T extends readonly PermissionType[]>(
3846
entityId?: number,
3947
enabled: boolean = true
4048
): PermissionsResult<T> => {
41-
const { isIAMEnabled } = useIsIAMEnabled();
49+
const { isIAMBeta, isIAMEnabled } = useIsIAMEnabled();
50+
const { data: profile } = useProfile();
51+
52+
/**
53+
* BETA and LA features should use the new permission model.
54+
* However, beta features are limited to a subset of AccessTypes and account permissions.
55+
* - Use Beta Permissions if:
56+
* - The feature is beta
57+
* - The access type is in the BETA_ACCESS_TYPE_SCOPE
58+
* - The account permission is not in the LA_ACCOUNT_ADMIN_PERMISSIONS_TO_EXCLUDE
59+
* - Use LA Permissions if:
60+
* - The feature is not beta
61+
*/
62+
const useBetaPermissions =
63+
isIAMEnabled &&
64+
isIAMBeta &&
65+
BETA_ACCESS_TYPE_SCOPE.includes(accessType) &&
66+
LA_ACCOUNT_ADMIN_PERMISSIONS_TO_EXCLUDE.some(
67+
(blacklistedPermission) =>
68+
permissionsToCheck.includes(blacklistedPermission as AccountAdmin) // some of the account admin in the blacklist have not been added yet
69+
) === false;
70+
const useLAPermissions = isIAMEnabled && !isIAMBeta;
71+
const shouldUsePermissionMap = useBetaPermissions || useLAPermissions;
72+
73+
const { data: grants } = useGrants(
74+
(!isIAMEnabled || !shouldUsePermissionMap) && enabled
75+
);
4276

4377
const { data: userAccountPermissions, ...restAccountPermissions } =
4478
useUserAccountPermissions(
45-
isIAMEnabled && accessType === 'account' && enabled
79+
shouldUsePermissionMap && accessType === 'account' && enabled
4680
);
4781

4882
const { data: userEntityPermissions, ...restEntityPermissions } =
49-
useUserEntityPermissions(accessType, entityId!, isIAMEnabled && enabled);
83+
useUserEntityPermissions(
84+
accessType,
85+
entityId!,
86+
shouldUsePermissionMap && enabled
87+
);
5088

5189
const usersPermissions =
5290
accessType === 'account' ? userAccountPermissions : userEntityPermissions;
5391

54-
const { data: profile } = useProfile();
55-
const { data: grants } = useGrants(!isIAMEnabled && enabled);
56-
57-
const permissionMap = isIAMEnabled
92+
const permissionMap = shouldUsePermissionMap
5893
? toPermissionMap(
5994
permissionsToCheck,
6095
usersPermissions!,

0 commit comments

Comments
 (0)