Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/api-v4/src/iam/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ export type AccountAdmin =
| 'view_account_login'
| 'view_account_settings'
| 'view_child_account'
| 'view_default_delegate_access'
| 'view_enrolled_beta_program'
| 'view_lock'
| 'view_network_usage'
Expand Down
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-13409-added-1771405985553.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Added
---

IAM Parent/Child: add permissions to UI ([#13409](https://github.com/linode/manager/pull/13409))
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,17 @@ const mocks = vi.hoisted(() => ({
mockUseGetChildAccountsQuery: vi.fn(),
useParams: vi.fn().mockReturnValue({}),
useSearch: vi.fn().mockReturnValue({}),
usePermissions: vi.fn().mockReturnValue({}),
}));

vi.mock('src/features/IAM/hooks/usePermissions', async () => {
const actual = await vi.importActual('src/features/IAM/hooks/usePermissions');
return {
...actual,
usePermissions: mocks.usePermissions,
};
});

vi.mock('@tanstack/react-router', async () => {
const actual = await vi.importActual('@tanstack/react-router');
return {
Expand Down Expand Up @@ -53,6 +62,10 @@ describe('AccountDelegations', () => {
data: { data: mockDelegations, results: mockDelegations.length },
isLoading: false,
});
mocks.usePermissions.mockReturnValue({
data: { list_all_child_accounts: true },
isLoading: false,
});
});

it('should render the delegations table with data', async () => {
Expand Down Expand Up @@ -93,4 +106,23 @@ describe('AccountDelegations', () => {
expect(emptyElement).toBeInTheDocument();
});
});

it('should not render if user does not have permissions', () => {
mocks.usePermissions.mockReturnValue({
data: {
list_all_child_accounts: false,
},
isLoading: false,
});

renderWithTheme(<AccountDelegations />, {
flags: { iamDelegation: { enabled: true }, iam: { enabled: true } },
initialRoute: '/iam',
});
expect(
screen.queryByText(
'You do not have permission to view account delegations.'
)
).toBeVisible();
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useGetChildAccountsQuery } from '@linode/queries';
import { CircleProgress, Paper, Stack } from '@linode/ui';
import { CircleProgress, Notice, Paper, Stack } from '@linode/ui';
import { useMediaQuery, useTheme } from '@mui/material';
import { useNavigate, useSearch } from '@tanstack/react-router';
import React from 'react';
Expand All @@ -10,13 +10,18 @@ import { useOrderV2 } from 'src/hooks/useOrderV2';
import { usePaginationV2 } from 'src/hooks/usePaginationV2';

import { useIsIAMDelegationEnabled } from '../hooks/useIsIAMEnabled';
import { usePermissions } from '../hooks/usePermissions';
import { AccountDelegationsTable } from './AccountDelegationsTable';

const DELEGATIONS_ROUTE = '/iam/delegations';

export const AccountDelegations = () => {
const navigate = useNavigate();
const { isIAMDelegationEnabled } = useIsIAMDelegationEnabled();
const { data: permissions, isLoading: isPermissionsLoading } = usePermissions(
'account',
['list_all_child_accounts']
);

const { company } = useSearch({
from: '/iam',
Expand Down Expand Up @@ -78,12 +83,22 @@ export const AccountDelegations = () => {
});
};

if (isLoading) {
if (isLoading || isPermissionsLoading) {
return <CircleProgress />;
}

if (!permissions?.list_all_child_accounts) {
return (
<Notice variant="error">
You do not have permission to view account delegations.
</Notice>
);
}

if (!isIAMDelegationEnabled) {
return null;
}

return (
<Paper sx={(theme) => ({ marginTop: theme.tokens.spacing.S16 })}>
<Stack
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const queryMocks = vi.hoisted(() => ({
useIsDefaultDelegationRolesForChildAccount: vi
.fn()
.mockReturnValue({ isDefaultDelegationRolesForChildAccount: true }),
usePermissions: vi.fn().mockReturnValue({}),
}));

vi.mock('src/features/IAM/hooks/useDelegationRole', () => ({
Expand All @@ -41,6 +42,14 @@ vi.mock('src/queries/entities/entities', async () => {
};
});

vi.mock('src/features/IAM/hooks/usePermissions', async () => {
const actual = await vi.importActual('src/features/IAM/hooks/usePermissions');
return {
...actual,
usePermissions: queryMocks.usePermissions,
};
});

vi.mock('@tanstack/react-router', async () => {
const actual = await vi.importActual('@tanstack/react-router');
return {
Expand All @@ -51,6 +60,14 @@ vi.mock('@tanstack/react-router', async () => {
});

describe('DefaultEntityAccess', () => {
beforeEach(() => {
vi.clearAllMocks();

queryMocks.usePermissions.mockReturnValue({
data: { view_default_delegate_access: true },
isLoading: false,
});
});
it('should render', async () => {
queryMocks.useGetDefaultDelegationAccessQuery.mockReturnValue({
data: {
Expand Down Expand Up @@ -92,4 +109,21 @@ describe('DefaultEntityAccess', () => {
renderWithTheme(<DefaultEntityAccess />);
expect(screen.getByText(ERROR_STATE_TEXT)).toBeVisible();
});

it('should not render if user does not have permissions', () => {
queryMocks.usePermissions.mockReturnValue({
data: {
view_default_delegate_access: false,
},
isLoading: false,
});

renderWithTheme(<DefaultEntityAccess />);

expect(
screen.queryByText(
'You do not have permission to view default entity access for delegate users.'
)
).toBeVisible();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import { useGetDefaultDelegationAccessQuery } from '@linode/queries';
import {
CircleProgress,
ErrorState,
Notice,
Paper,
Stack,
Typography,
} from '@linode/ui';
import * as React from 'react';

import { usePermissions } from '../../hooks/usePermissions';
import { AssignedEntitiesTable } from '../../Shared/AssignedEntitiesTable/AssignedEntitiesTable';
import {
ERROR_STATE_TEXT,
Expand All @@ -16,20 +18,35 @@ import {
import { NoAssignedRoles } from '../../Shared/NoAssignedRoles/NoAssignedRoles';

export const DefaultEntityAccess = () => {
const { data: permissions, isLoading: isPermissionsLoading } = usePermissions(
'account',
['view_default_delegate_access']
);
const {
data: defaultAccess,
isLoading: defaultAccessLoading,
error,
} = useGetDefaultDelegationAccessQuery({ enabled: true });
} = useGetDefaultDelegationAccessQuery({
enabled: permissions?.view_default_delegate_access,
});

const hasAssignedEntities = defaultAccess
? defaultAccess.entity_access.length > 0
: false;

if (defaultAccessLoading) {
if (defaultAccessLoading || isPermissionsLoading) {
return <CircleProgress />;
}

if (!permissions?.view_default_delegate_access) {
return (
<Notice variant="error">
You do not have permission to view default entity access for delegate
users.
</Notice>
);
}

if (error) {
return <ErrorState errorText={ERROR_STATE_TEXT} />;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const queryMocks = vi.hoisted(() => ({
useIsDefaultDelegationRolesForChildAccount: vi
.fn()
.mockReturnValue({ isDefaultDelegationRolesForChildAccount: true }),
usePermissions: vi.fn().mockReturnValue({}),
}));

vi.mock('@tanstack/react-router', async () => {
Expand All @@ -35,11 +36,28 @@ vi.mock('@linode/queries', async () => {
queryMocks.useGetDefaultDelegationAccessQuery,
};
});

vi.mock('src/features/IAM/hooks/usePermissions', async () => {
const actual = await vi.importActual('src/features/IAM/hooks/usePermissions');
return {
...actual,
usePermissions: queryMocks.usePermissions,
};
});

vi.mock('src/features/IAM/hooks/useDelegationRole', () => ({
useIsDefaultDelegationRolesForChildAccount:
queryMocks.useIsDefaultDelegationRolesForChildAccount,
}));
describe('DefaultRoles', () => {
beforeEach(() => {
vi.clearAllMocks();

queryMocks.usePermissions.mockReturnValue({
data: { view_default_delegate_access: true },
isLoading: false,
});
});
it('should render', async () => {
queryMocks.useGetDefaultDelegationAccessQuery.mockReturnValue({
data: {
Expand Down Expand Up @@ -84,4 +102,21 @@ describe('DefaultRoles', () => {
renderWithTheme(<DefaultRoles />);
expect(screen.getByText(ERROR_STATE_TEXT)).toBeVisible();
});

it('should not render if user does not have permissions', () => {
queryMocks.usePermissions.mockReturnValue({
data: {
view_default_delegate_access: false,
},
isLoading: false,
});

renderWithTheme(<DefaultRoles />);

expect(
screen.queryByText(
'You do not have permission to view default roles for delegate users.'
)
).toBeVisible();
});
});
28 changes: 25 additions & 3 deletions packages/manager/src/features/IAM/Roles/Defaults/DefaultRoles.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { useGetDefaultDelegationAccessQuery } from '@linode/queries';
import { CircleProgress, ErrorState, Paper, Typography } from '@linode/ui';
import {
CircleProgress,
ErrorState,
Notice,
Paper,
Typography,
} from '@linode/ui';
import * as React from 'react';

import { usePermissions } from '../../hooks/usePermissions';
import { AssignedRolesTable } from '../../Shared/AssignedRolesTable/AssignedRolesTable';
import {
ERROR_STATE_TEXT,
Expand All @@ -10,20 +17,35 @@ import {
import { NoAssignedRoles } from '../../Shared/NoAssignedRoles/NoAssignedRoles';

export const DefaultRoles = () => {
const { data: permissions, isLoading: isPermissionsLoading } = usePermissions(
'account',
['view_default_delegate_access']
);
const {
data: defaultRolesData,
isLoading: defaultRolesLoading,
error,
} = useGetDefaultDelegationAccessQuery({ enabled: true });
} = useGetDefaultDelegationAccessQuery({
enabled: permissions?.view_default_delegate_access,
});

const hasAssignedRoles = defaultRolesData
? defaultRolesData.account_access.length > 0 ||
defaultRolesData.entity_access.length > 0
: false;

if (defaultRolesLoading) {
if (defaultRolesLoading || isPermissionsLoading) {
return <CircleProgress />;
}

if (!permissions?.view_default_delegate_access) {
return (
<Notice variant="error">
You do not have permission to view default roles for delegate users.
</Notice>
);
}

if (error) {
return <ErrorState errorText={ERROR_STATE_TEXT} />;
}
Expand Down
7 changes: 5 additions & 2 deletions packages/manager/src/features/IAM/Roles/Roles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ export const RolesLanding = () => {
permissions?.list_role_permissions
);
const { isIAMDelegationEnabled } = useIsIAMDelegationEnabled();
const { isChildUserType, isProfileLoading } = useDelegationRole();
const { isChildUserType, isProfileLoading, isDelegateUserType } =
useDelegationRole();

const { roles } = React.useMemo(() => {
if (!accountRoles) {
Expand All @@ -41,7 +42,9 @@ export const RolesLanding = () => {

return (
<>
{isChildUserType && isIAMDelegationEnabled && <DefaultRolesPanel />}
{(isChildUserType || isDelegateUserType) && isIAMDelegationEnabled && (
<DefaultRolesPanel />
)}
<Paper sx={(theme) => ({ marginTop: theme.tokens.spacing.S16 })}>
<Typography variant="h2">Roles</Typography>
<RolesTable roles={roles} />
Expand Down
Loading