Skip to content

Commit 2dffce0

Browse files
corya-akamaiabailly-akamaiConal Ryan
authored
feat: [UIE-9051] - IAM RBAC block non-beta route access (linode#12656)
* redirect at route level * cleanup * explore fetching at the route level * Add a few more redirects * improve check based on @bnussman-akamai & fix roles redirect * Added changeset: IAM RBAC block non-beta route access --------- Co-authored-by: Alban Bailly <[email protected]> Co-authored-by: Alban Bailly <[email protected]> Co-authored-by: Conal Ryan <[email protected]>
1 parent 00cb2bf commit 2dffce0

File tree

9 files changed

+189
-28
lines changed

9 files changed

+189
-28
lines changed
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+
IAM RBAC block non-beta route access ([#12656](https://github.com/linode/manager/pull/12656))

packages/manager/src/Router.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useQueryClient } from '@tanstack/react-query';
33
import { RouterProvider } from '@tanstack/react-router';
44
import * as React from 'react';
55

6+
import { useFlags } from 'src/hooks/useFlags';
67
import { useGlobalErrors } from 'src/hooks/useGlobalErrors';
78

89
import { useIsACLPEnabled } from './features/CloudPulse/Utils/utils';
@@ -19,11 +20,13 @@ export const Router = () => {
1920
const { isDatabasesEnabled } = useIsDatabasesEnabled();
2021
const { isPlacementGroupsEnabled } = useIsPlacementGroupsEnabled();
2122
const { isACLPEnabled } = useIsACLPEnabled();
23+
const flags = useFlags();
2224

2325
// Update the router's context
2426
router.update({
2527
context: {
2628
accountSettings,
29+
flags,
2730
globalErrors,
2831
isACLPEnabled,
2932
isDatabasesEnabled,

packages/manager/src/features/IAM/IAMLanding.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Outlet } from '@tanstack/react-router';
1+
import { Outlet, useLocation, useNavigate } from '@tanstack/react-router';
22
import * as React from 'react';
33

44
import { LandingHeader } from 'src/components/LandingHeader';
@@ -11,6 +11,9 @@ import { useTabs } from 'src/hooks/useTabs';
1111
import { IAM_DOCS_LINK } from './Shared/constants';
1212

1313
export const IdentityAccessLanding = React.memo(() => {
14+
const location = useLocation();
15+
const navigate = useNavigate();
16+
1417
const { tabs, tabIndex, handleTabChange } = useTabs([
1518
{
1619
to: `/iam/users`,
@@ -31,6 +34,10 @@ export const IdentityAccessLanding = React.memo(() => {
3134
title: 'Identity and Access',
3235
};
3336

37+
if (location.pathname === '/iam') {
38+
navigate({ to: '/iam/users' });
39+
}
40+
3441
return (
3542
<>
3643
<LandingHeader {...landingHeaderProps} spacingBottom={4} />

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1+
import { iamQueries, profileQueries } from '@linode/queries';
12
import {
23
useAccountRoles,
34
useProfile,
45
useUserAccountPermissions,
56
} from '@linode/queries';
7+
import { queryOptions } from '@tanstack/react-query';
68

79
import { useFlags } from 'src/hooks/useFlags';
810

11+
import type { QueryClient } from '@tanstack/react-query';
12+
import type { FlagSet } from 'src/featureFlags';
13+
914
/**
1015
* Hook to determine if the IAM feature is enabled for the current user.
1116
*
@@ -27,3 +32,44 @@ export const useIsIAMEnabled = () => {
2732
isIAMEnabled: flags?.iam?.enabled && Boolean(roles || permissions?.length),
2833
};
2934
};
35+
36+
/**
37+
* This function is an alternative to the useIsIAMEnabled hook to be used in our router's beforeLoad functions.
38+
* The logic is identical, but here we fetch at the router level instead of the hook level.
39+
* This does not over-fetch data since the components will do a cache lookup in subsequent renders.
40+
* This is only used in a a few routes for iam/account specific redirect purposes.
41+
*
42+
* NOTE: we could use this in the `loader` method (instead of `beforeLoad`) and have the component use the `useLoaderData` hook,
43+
* but there isn't at the moment a big advantage of doing that since these are isolated routes.
44+
*/
45+
export const checkIAMEnabled = async (
46+
queryClient: QueryClient,
47+
flags: FlagSet
48+
): Promise<boolean> => {
49+
if (!flags?.iam?.enabled) {
50+
return false;
51+
}
52+
53+
try {
54+
const profile = await queryClient.ensureQueryData(
55+
queryOptions(profileQueries.profile())
56+
);
57+
58+
if (profile.restricted) {
59+
// For restricted users ONLY, get permissions
60+
const permissions = await queryClient.ensureQueryData(
61+
queryOptions(iamQueries.user(profile.username)._ctx.accountPermissions)
62+
);
63+
return Boolean(permissions.length);
64+
}
65+
66+
// For non-restricted users ONLY, get roles
67+
const roles = await queryClient.ensureQueryData(
68+
queryOptions(iamQueries.accountRoles)
69+
);
70+
71+
return Boolean(roles);
72+
} catch {
73+
return false;
74+
}
75+
};

packages/manager/src/routes/IAM/IAMRoute.tsx

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,11 @@
1-
import { NotFound } from '@linode/ui';
21
import { Outlet } from '@tanstack/react-router';
32
import React from 'react';
43

54
import { DocumentTitleSegment } from 'src/components/DocumentTitle';
65
import { ProductInformationBanner } from 'src/components/ProductInformationBanner/ProductInformationBanner';
76
import { SuspenseLoader } from 'src/components/SuspenseLoader';
8-
import { useIsIAMEnabled } from 'src/features/IAM/hooks/useIsIAMEnabled';
97

108
export const IAMRoute = () => {
11-
const { isIAMEnabled } = useIsIAMEnabled();
12-
13-
if (!isIAMEnabled) {
14-
return <NotFound />;
15-
}
16-
179
return (
1810
<React.Suspense fallback={<SuspenseLoader />}>
1911
<DocumentTitleSegment segment="Identity and Access" />

packages/manager/src/routes/IAM/index.ts

Lines changed: 80 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { createRoute, redirect } from '@tanstack/react-router';
22

3+
import { checkIAMEnabled } from 'src/features/IAM/hooks/useIsIAMEnabled';
4+
35
import { rootRoute } from '../root';
46
import { IAMRoute } from './IAMRoute';
57

@@ -18,11 +20,7 @@ const iamRoute = createRoute({
1820
getParentRoute: () => rootRoute,
1921
validateSearch: (search: IamUsersSearchParams) => search,
2022
path: 'iam',
21-
}).lazy(() =>
22-
import('src/features/IAM/iamLandingLazyRoute').then(
23-
(m) => m.iamLandingLazyRoute
24-
)
25-
);
23+
});
2624

2725
const iamCatchAllRoute = createRoute({
2826
getParentRoute: () => iamRoute,
@@ -32,10 +30,7 @@ const iamCatchAllRoute = createRoute({
3230
},
3331
});
3432

35-
const iamIndexRoute = createRoute({
36-
beforeLoad: async () => {
37-
throw redirect({ to: '/iam/users' });
38-
},
33+
const iamTabsRoute = createRoute({
3934
getParentRoute: () => iamRoute,
4035
path: '/',
4136
}).lazy(() =>
@@ -45,8 +40,20 @@ const iamIndexRoute = createRoute({
4540
);
4641

4742
const iamUsersRoute = createRoute({
48-
getParentRoute: () => iamRoute,
43+
getParentRoute: () => iamTabsRoute,
4944
path: 'users',
45+
beforeLoad: async ({ context }) => {
46+
const isIAMEnabled = await checkIAMEnabled(
47+
context.queryClient,
48+
context.flags
49+
);
50+
51+
if (!isIAMEnabled) {
52+
throw redirect({
53+
to: '/account/users',
54+
});
55+
}
56+
},
5057
}).lazy(() =>
5158
import('src/features/IAM/Users/UsersTable/usersLandingLazyRoute').then(
5259
(m) => m.usersLandingLazyRoute
@@ -62,8 +69,20 @@ const iamUsersCatchAllRoute = createRoute({
6269
});
6370

6471
const iamRolesRoute = createRoute({
65-
getParentRoute: () => iamRoute,
72+
getParentRoute: () => iamTabsRoute,
6673
path: 'roles',
74+
beforeLoad: async ({ context }) => {
75+
const isIAMEnabled = await checkIAMEnabled(
76+
context.queryClient,
77+
context.flags
78+
);
79+
80+
if (!isIAMEnabled) {
81+
throw redirect({
82+
to: '/account/users',
83+
});
84+
}
85+
},
6786
}).lazy(() =>
6887
import('src/features/IAM/Roles/rolesLandingLazyRoute').then(
6988
(m) => m.rolesLandingLazyRoute
@@ -79,8 +98,8 @@ const iamRolesCatchAllRoute = createRoute({
7998
});
8099

81100
const iamUserNameRoute = createRoute({
82-
getParentRoute: () => rootRoute,
83-
path: 'iam/users/$username',
101+
getParentRoute: () => iamRoute,
102+
path: '/users/$username',
84103
}).lazy(() =>
85104
import('src/features/IAM/Users/userDetailsLandingLazyRoute').then(
86105
(m) => m.userDetailsLandingLazyRoute
@@ -105,6 +124,20 @@ const iamUserNameIndexRoute = createRoute({
105124
const iamUserNameDetailsRoute = createRoute({
106125
getParentRoute: () => iamUserNameRoute,
107126
path: 'details',
127+
beforeLoad: async ({ context, params }) => {
128+
const isIAMEnabled = await checkIAMEnabled(
129+
context.queryClient,
130+
context.flags
131+
);
132+
const { username } = params;
133+
134+
if (!isIAMEnabled && username) {
135+
throw redirect({
136+
to: '/account/users/$username/profile',
137+
params: { username },
138+
});
139+
}
140+
},
108141
}).lazy(() =>
109142
import('src/features/IAM/Users/UserDetails/userProfileLazyRoute').then(
110143
(m) => m.userProfileLazyRoute
@@ -114,6 +147,20 @@ const iamUserNameDetailsRoute = createRoute({
114147
const iamUserNameRolesRoute = createRoute({
115148
getParentRoute: () => iamUserNameRoute,
116149
path: 'roles',
150+
beforeLoad: async ({ context, params }) => {
151+
const isIAMEnabled = await checkIAMEnabled(
152+
context.queryClient,
153+
context.flags
154+
);
155+
const { username } = params;
156+
157+
if (!isIAMEnabled && username) {
158+
throw redirect({
159+
to: '/account/users/$username/permissions',
160+
params: { username },
161+
});
162+
}
163+
},
117164
}).lazy(() =>
118165
import('src/features/IAM/Users/UserRoles/userRolesLazyRoute').then(
119166
(m) => m.userRolesLazyRoute
@@ -124,6 +171,20 @@ const iamUserNameEntitiesRoute = createRoute({
124171
getParentRoute: () => iamUserNameRoute,
125172
path: 'entities',
126173
validateSearch: (search: IamEntitiesSearchParams) => search,
174+
beforeLoad: async ({ context, params }) => {
175+
const isIAMEnabled = await checkIAMEnabled(
176+
context.queryClient,
177+
context.flags
178+
);
179+
const { username } = params;
180+
181+
if (!isIAMEnabled && username) {
182+
throw redirect({
183+
to: '/account/users/$username',
184+
params: { username },
185+
});
186+
}
187+
},
127188
}).lazy(() =>
128189
import('src/features/IAM/Users/UserEntities/userEntitiesLazyRoute').then(
129190
(m) => m.userEntitiesLazyRoute
@@ -178,12 +239,13 @@ const iamUserNameEntitiesCatchAllRoute = createRoute({
178239
});
179240

180241
export const iamRouteTree = iamRoute.addChildren([
181-
iamIndexRoute,
182-
iamRolesRoute,
183-
iamUsersRoute,
242+
iamTabsRoute.addChildren([
243+
iamRolesRoute,
244+
iamUsersRoute,
245+
iamUsersCatchAllRoute,
246+
iamRolesCatchAllRoute,
247+
]),
184248
iamCatchAllRoute,
185-
iamUsersCatchAllRoute,
186-
iamRolesCatchAllRoute,
187249
iamUserNameRoute.addChildren([
188250
iamUserNameIndexRoute,
189251
iamUserNameDetailsRoute,

packages/manager/src/routes/account/index.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { createRoute } from '@tanstack/react-router';
1+
import { createRoute, redirect } from '@tanstack/react-router';
2+
3+
import { checkIAMEnabled } from 'src/features/IAM/hooks/useIsIAMEnabled';
24

35
import { rootRoute } from '../root';
46
import { AccountRoute } from './AccountRoute';
@@ -38,6 +40,16 @@ const accountBillingRoute = createRoute({
3840
const accountUsersRoute = createRoute({
3941
getParentRoute: () => accountTabsRoute,
4042
path: '/users',
43+
beforeLoad: async ({ context }) => {
44+
const isIAMEnabled = await checkIAMEnabled(
45+
context.queryClient,
46+
context.flags
47+
);
48+
49+
if (isIAMEnabled) {
50+
throw redirect({ to: '/iam/users' });
51+
}
52+
},
4153
}).lazy(() =>
4254
import('src/features/Users/usersLandingLazyRoute').then(
4355
(m) => m.usersLandingLazyRoute
@@ -92,6 +104,32 @@ const accountSettingsRoute = createRoute({
92104
const accountUsersUsernameRoute = createRoute({
93105
getParentRoute: () => accountRoute,
94106
path: '/users/$username',
107+
beforeLoad: async ({ context, params, location }) => {
108+
const { username } = params;
109+
110+
const isIAMEnabled = await checkIAMEnabled(
111+
context.queryClient,
112+
context.flags
113+
);
114+
115+
if (!isIAMEnabled || !username) {
116+
return;
117+
}
118+
119+
if (location.pathname.endsWith('/permissions')) {
120+
throw redirect({
121+
to: '/iam/users/$username/roles',
122+
params: { username },
123+
replace: true,
124+
});
125+
}
126+
127+
throw redirect({
128+
to: '/iam/users/$username/details',
129+
params: { username },
130+
replace: true,
131+
});
132+
},
95133
}).lazy(() =>
96134
import('src/features/Users/userDetailLazyRoute').then(
97135
(m) => m.userDetailLazyRoute

packages/manager/src/routes/index.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,12 @@ export const routeTree = rootRoute.addChildren([
8181

8282
export const router = createRouter({
8383
context: {
84+
accountSettings: undefined,
85+
flags: {},
86+
globalErrors: {},
87+
isACLPEnabled: false,
88+
isDatabasesEnabled: false,
89+
isPlacementGroupsEnabled: false,
8490
queryClient: new QueryClient(),
8591
},
8692
defaultNotFoundComponent: () => <NotFound />,

packages/manager/src/routes/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import type { AccountSettings } from '@linode/api-v4';
22
import type { QueryClient } from '@tanstack/react-query';
3+
import type { FlagSet } from 'src/featureFlags';
34

45
export type RouterContext = {
56
accountSettings?: AccountSettings;
7+
flags: FlagSet;
68
globalErrors?: {
79
account_unactivated?: boolean;
810
};

0 commit comments

Comments
 (0)