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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Changed
---

IAM RBAC: fix permission check for rebuilding and resizing linode ([#12680](https://github.com/linode/manager/pull/12680))
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ import { renderWithTheme } from 'src/utilities/testHelpers';

import { LinodeRebuildForm } from './LinodeRebuildForm';

const queryMocks = vi.hoisted(() => ({
userPermissions: vi.fn(() => ({
data: {
rebuild_linode: false,
},
})),
}));

vi.mock('src/features/IAM/hooks/usePermissions', () => ({
usePermissions: queryMocks.userPermissions,
}));

describe('LinodeRebuildForm', () => {
it('renders a notice reccomending users add user data when the Linode already uses user data', async () => {
const linode = linodeFactory.build({ has_user_data: true });
Expand Down Expand Up @@ -48,4 +60,48 @@ describe('LinodeRebuildForm', () => {
getByLabelText('This Linode does not have existing user data.')
).toBeVisible();
});

it('should disable all fields if user does not have permission', async () => {
const linode = linodeFactory.build();

const { getByRole, getByPlaceholderText, getAllByRole } = renderWithTheme(
<LinodeRebuildForm linode={linode} onSuccess={vi.fn()} />
);

const passwordInput = getByPlaceholderText('Enter a password.');
expect(passwordInput).toBeDisabled();

const rebuildBtn = getByRole('button', {
name: 'Rebuild Linode',
});
expect(rebuildBtn).toHaveAttribute('aria-disabled', 'true');

const rebuildInput = getAllByRole('combobox')[0];
expect(rebuildInput).toBeDisabled();
});

it('should enable all fields if user has permission', async () => {
const linode = linodeFactory.build();

queryMocks.userPermissions.mockReturnValue({
data: {
rebuild_linode: true,
},
});

const { getByRole, getByPlaceholderText, getAllByRole } = renderWithTheme(
<LinodeRebuildForm linode={linode} onSuccess={vi.fn()} />
);

const passwordInput = getByPlaceholderText('Enter a password.');
expect(passwordInput).toBeEnabled();

const rebuildBtn = getByRole('button', {
name: 'Rebuild Linode',
});
expect(rebuildBtn).not.toHaveAttribute('aria-disabled', 'true');

const rebuildInput = getAllByRole('combobox')[0];
expect(rebuildInput).toBeEnabled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { useSnackbar } from 'notistack';
import React, { useEffect, useRef, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';

import { useIsResourceRestricted } from 'src/hooks/useIsResourceRestricted';
import { usePermissions } from 'src/features/IAM/hooks/usePermissions';
import { useEventsPollingActions } from 'src/queries/events/events';

import { StackScriptSelectionList } from '../../LinodeCreate/Tabs/StackScripts/StackScriptSelectionList';
Expand Down Expand Up @@ -43,11 +43,11 @@ export const LinodeRebuildForm = (props: Props) => {

const [type, setType] = useState<LinodeRebuildType>('Image');

const isLinodeReadOnly = useIsResourceRestricted({
grantLevel: 'read_only',
grantType: 'linode',
id: linode.id,
});
const { data: permissions } = usePermissions(
'linode',
['rebuild_linode'],
linode.id
);

const { data: isTypeToConfirmEnabled } = usePreferences(
(preferences) => preferences?.type_to_confirm ?? true
Expand Down Expand Up @@ -127,7 +127,7 @@ export const LinodeRebuildForm = (props: Props) => {
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<Stack spacing={2}>
{isLinodeReadOnly && <LinodePermissionsError />}
{!permissions.rebuild_linode && <LinodePermissionsError />}
{form.formState.errors.root && (
<Notice text={form.formState.errors.root.message} variant="error" />
)}
Expand All @@ -150,7 +150,7 @@ export const LinodeRebuildForm = (props: Props) => {
}}
>
<RebuildFromSelect
disabled={isLinodeReadOnly}
disabled={!permissions.rebuild_linode}
setType={setType}
type={type}
/>
Expand All @@ -167,21 +167,21 @@ export const LinodeRebuildForm = (props: Props) => {
<StackScriptSelectionList type="Community" />
)}
{type.includes('StackScript') && <UserDefinedFields />}
<Image disabled={isLinodeReadOnly} />
<Password disabled={isLinodeReadOnly} />
<SSHKeys disabled={isLinodeReadOnly} />
<Image disabled={!permissions.rebuild_linode} />
<Password disabled={!permissions.rebuild_linode} />
<SSHKeys disabled={!permissions.rebuild_linode} />
<DiskEncryption
disabled={isLinodeReadOnly}
disabled={!permissions.rebuild_linode}
isLKELinode={linode.lke_cluster_id !== null}
linodeRegion={linode.region}
/>
<UserData disabled={isLinodeReadOnly} linode={linode} />
<UserData disabled={!permissions.rebuild_linode} linode={linode} />
<Confirmation
disabled={isLinodeReadOnly}
disabled={!permissions.rebuild_linode}
linodeLabel={linode.label}
/>
</Stack>
<Actions disabled={isLinodeReadOnly} />
<Actions disabled={!permissions.rebuild_linode} />
</Stack>
</form>
</FormProvider>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { waitFor } from '@testing-library/react';
import * as React from 'react';

import { extDisk, swapDisk } from 'src/__data__/disks';
Expand All @@ -19,6 +20,18 @@ const props: Props = {
open: true,
};

const queryMocks = vi.hoisted(() => ({
userPermissions: vi.fn(() => ({
data: {
resize_linode: false,
},
})),
}));

vi.mock('src/features/IAM/hooks/usePermissions', () => ({
usePermissions: queryMocks.userPermissions,
}));

beforeAll(() => {
mockMatchMedia();
});
Expand Down Expand Up @@ -101,4 +114,30 @@ describe('LinodeResize', () => {
});
});
});

it('should not allow resizing if user does not have permission', async () => {
const { findByText, getByRole } = renderWithTheme(
<LinodeResize {...props} />
);
await findByText(
"You don't have permissions to edit this Linode. Please contact your account administrator to request the necessary permissions."
);

const resizeBtn = getByRole('button', { name: 'Resize Linode' });
expect(resizeBtn).toBeDisabled();
});

it('should not render LinodePermissionsError when user has resize_linode permission', async () => {
queryMocks.userPermissions.mockReturnValue({
data: {
resize_linode: true,
},
});

const { queryByTestId } = renderWithTheme(<LinodeResize {...props} />);

await waitFor(() => {
expect(queryByTestId('linode-permissions-error')).not.toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ import { ErrorMessage } from 'src/components/ErrorMessage';
import { Link } from 'src/components/Link';
import { TypeToConfirm } from 'src/components/TypeToConfirm/TypeToConfirm';
import { PlansPanel } from 'src/features/components/PlansPanel/PlansPanel';
import { usePermissions } from 'src/features/IAM/hooks/usePermissions';
import { linodeInTransition } from 'src/features/Linodes/transitions';
import { useIsResourceRestricted } from 'src/hooks/useIsResourceRestricted';
import { useEventsPollingActions } from 'src/queries/events/events';
import { extendType } from 'src/utilities/extendType';

Expand Down Expand Up @@ -96,11 +96,11 @@ export const LinodeResize = (props: Props) => {
const hostMaintenance = linode?.status === 'stopped';
const isLinodeOffline = linode?.status === 'offline';

const isLinodesGrantReadOnly = useIsResourceRestricted({
grantLevel: 'read_only',
grantType: 'linode',
id: linodeId,
});
const { data: permissions } = usePermissions(
'linode',
['resize_linode'],
linodeId
);

const formik = useFormik<ResizeLinodePayload>({
initialValues: {
Expand Down Expand Up @@ -164,7 +164,7 @@ export const LinodeResize = (props: Props) => {
}
}, [error]);

const tableDisabled = hostMaintenance || isLinodesGrantReadOnly;
const tableDisabled = hostMaintenance || !permissions.resize_linode;

const submitButtonDisabled =
Boolean(typeToConfirmPreference) && confirmationText !== linode?.label;
Expand Down Expand Up @@ -195,7 +195,7 @@ export const LinodeResize = (props: Props) => {
<CircleProgress />
) : (
<form onSubmit={formik.handleSubmit} ref={formRef}>
{isLinodesGrantReadOnly && <LinodePermissionsError />}
{!permissions.resize_linode && <LinodePermissionsError />}
{hostMaintenance && <HostMaintenanceError />}
{disksError && (
<Notice
Expand Down