Skip to content

Commit

Permalink
fix: [M3-8590] - Restrict access to Database Create page for restrict…
Browse files Browse the repository at this point in the history
…ed users (#11137)

* Restrict access to Database Create page for restricted users

* updated DatabaseCreate tests

* Added notice for restricted users on Database Create page

* Fix

* Added changeset: Database create page form being enabled for restricted users

* Addressed PR comments
  • Loading branch information
zaenab-akamai authored Oct 28, 2024
1 parent 73c3342 commit b5da866
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 11 deletions.
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-11137-fixed-1729745562099.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Fixed
---

Database create page form being enabled for restricted users ([#11137](https://github.com/linode/manager/pull/11137))
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@ import DatabaseCreate from './DatabaseCreate';

const loadingTestId = 'circle-progress';

const queryMocks = vi.hoisted(() => ({
useProfile: vi.fn().mockReturnValue({ data: { restricted: false } }),
}));

vi.mock('src/queries/profile/profile', async () => {
const actual = await vi.importActual('src/queries/profile/profile');
return {
...actual,
useProfile: queryMocks.useProfile,
};
});

beforeAll(() => mockMatchMedia());

describe('Database Create', () => {
Expand Down Expand Up @@ -154,4 +166,96 @@ describe('Database Create', () => {
expect(nodeRadioBtns).toHaveTextContent('$100/month $0.15/hr');
expect(nodeRadioBtns).toHaveTextContent('$140/month $0.21/hr');
});

it('should have the "Create Database Cluster" button disabled for restricted users', async () => {
queryMocks.useProfile.mockReturnValue({ data: { restricted: true } });

const { findByText, getByTestId } = renderWithTheme(<DatabaseCreate />);

expect(getByTestId(loadingTestId)).toBeInTheDocument();

await waitForElementToBeRemoved(getByTestId(loadingTestId));
const createClusterButtonSpan = await findByText('Create Database Cluster');
const createClusterButton = createClusterButtonSpan.closest('button');

expect(createClusterButton).toBeInTheDocument();
expect(createClusterButton).toBeDisabled();
});

it('should disable form inputs for restricted users', async () => {
queryMocks.useProfile.mockReturnValue({ data: { restricted: true } });

const {
findAllByRole,
findAllByTestId,
findByPlaceholderText,
getByTestId,
} = renderWithTheme(<DatabaseCreate />);

expect(getByTestId(loadingTestId)).toBeInTheDocument();

await waitForElementToBeRemoved(getByTestId(loadingTestId));
const textInputs = await findAllByTestId('textfield-input');
textInputs.forEach((input: HTMLInputElement) => {
expect(input).toBeDisabled();
});

const dbEngineSelect = await findByPlaceholderText(
'Select a Database Engine'
);
expect(dbEngineSelect).toBeDisabled();
const regionSelect = await findByPlaceholderText('Select a Region');
expect(regionSelect).toBeDisabled();

const radioButtons = await findAllByRole('radio');
radioButtons.forEach((radioButton: HTMLElement) => {
expect(radioButton).toBeDisabled();
});
});

it('should have the "Create Database Cluster" button enabled for users with full access', async () => {
queryMocks.useProfile.mockReturnValue({ data: { restricted: false } });

const { findByText, getByTestId } = renderWithTheme(<DatabaseCreate />);

expect(getByTestId(loadingTestId)).toBeInTheDocument();

await waitForElementToBeRemoved(getByTestId(loadingTestId));
const createClusterButtonSpan = await findByText('Create Database Cluster');
const createClusterButton = createClusterButtonSpan.closest('button');

expect(createClusterButton).toBeInTheDocument();
expect(createClusterButton).toBeEnabled();
});

it('should enable form inputs for users with full access', async () => {
queryMocks.useProfile.mockReturnValue({ data: { restricted: false } });

const {
findAllByRole,
findAllByTestId,
findByPlaceholderText,
getByTestId,
} = renderWithTheme(<DatabaseCreate />);

expect(getByTestId(loadingTestId)).toBeInTheDocument();

await waitForElementToBeRemoved(getByTestId(loadingTestId));
const textInputs = await findAllByTestId('textfield-input');
textInputs.forEach((input: HTMLInputElement) => {
expect(input).toBeEnabled();
});

const dbEngineSelect = await findByPlaceholderText(
'Select a Database Engine'
);
expect(dbEngineSelect).toBeEnabled();
const regionSelect = await findByPlaceholderText('Select a Region');
expect(regionSelect).toBeEnabled();

const radioButtons = await findAllByRole('radio');
radioButtons.forEach((radioButton: HTMLElement) => {
expect(radioButton).toBeEnabled();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@ import { RegionSelect } from 'src/components/RegionSelect/RegionSelect';
import { RegionHelperText } from 'src/components/SelectRegionPanel/RegionHelperText';
import { TextField } from 'src/components/TextField';
import { Typography } from 'src/components/Typography';
import { getRestrictedResourceText } from 'src/features/Account/utils';
import { PlansPanel } from 'src/features/components/PlansPanel/PlansPanel';
import { EngineOption } from 'src/features/Databases/DatabaseCreate/EngineOption';
import { DatabaseLogo } from 'src/features/Databases/DatabaseLanding/DatabaseLogo';
import { databaseEngineMap } from 'src/features/Databases/DatabaseLanding/DatabaseRow';
import { useIsDatabasesEnabled } from 'src/features/Databases/utilities';
import { enforceIPMasks } from 'src/features/Firewalls/FirewallDetail/Rules/FirewallRuleDrawer.utils';
import { typeLabelDetails } from 'src/features/Linodes/presentation';
import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck';
import {
useCreateDatabaseMutation,
useDatabaseEnginesQuery,
Expand All @@ -48,6 +50,8 @@ import { getSelectedOptionFromGroupedOptions } from 'src/utilities/getSelectedOp
import { validateIPs } from 'src/utilities/ipUtils';
import { scrollErrorIntoViewV2 } from 'src/utilities/scrollErrorIntoViewV2';

import { DatabaseCreateAccessControls } from './DatabaseCreateAccessControls';

import type {
ClusterSize,
ComprehensiveReplicationType,
Expand All @@ -62,7 +66,6 @@ import type { Theme } from '@mui/material/styles';
import type { Item } from 'src/components/EnhancedSelect/Select';
import type { PlanSelectionType } from 'src/features/components/PlansPanel/types';
import type { ExtendedIP } from 'src/utilities/ipUtils';
import { DatabaseCreateAccessControls } from './DatabaseCreateAccessControls';

const useStyles = makeStyles()((theme: Theme) => ({
btnCtn: {
Expand Down Expand Up @@ -197,6 +200,9 @@ const DatabaseCreate = () => {
const { classes } = useStyles();
const history = useHistory();
const { isDatabasesV2Beta, isDatabasesV2Enabled } = useIsDatabasesEnabled();
const isRestricted = useRestrictedGlobalGrantCheck({
globalGrantType: 'add_databases',
});

const {
data: regionsData,
Expand Down Expand Up @@ -510,6 +516,17 @@ const DatabaseCreate = () => {
}}
title="Create"
/>
{isRestricted && (
<Notice
text={getRestrictedResourceText({
action: 'create',
resourceType: 'Databases',
})}
important
spacingTop={16}
variant="error"
/>
)}
<Paper>
{createError && (
<Notice variant="error">
Expand All @@ -523,6 +540,7 @@ const DatabaseCreate = () => {
<Typography variant="h2">Name Your Cluster</Typography>
<TextField
data-qa-label-input
disabled={isRestricted}
errorText={errors.label}
label="Cluster Label"
onChange={(e) => setFieldValue('label', e.target.value)}
Expand All @@ -544,6 +562,7 @@ const DatabaseCreate = () => {
)}
className={classes.engineSelect}
components={{ Option: EngineOption, SingleValue: _SingleValue }}
disabled={isRestricted}
errorText={errors.engine}
isClearable={false}
label="Database Engine"
Expand All @@ -555,6 +574,7 @@ const DatabaseCreate = () => {
<RegionSelect
currentCapability="Managed Databases"
disableClearable
disabled={isRestricted}
errorText={errors.region}
onChange={(e, region) => setFieldValue('region', region.id)}
regions={regionsData}
Expand All @@ -570,6 +590,7 @@ const DatabaseCreate = () => {
}}
className={classes.selectPlanPanel}
data-qa-select-plan
disabled={isRestricted}
error={errors.type}
handleTabChange={handleTabChange}
header="Choose a Plan"
Expand Down Expand Up @@ -599,11 +620,13 @@ const DatabaseCreate = () => {
);
}}
data-testid="database-nodes"
disabled={isRestricted}
>
{errors.cluster_size ? (
<Notice text={errors.cluster_size} variant="error" />
) : null}
<RadioGroup
aria-disabled={isRestricted}
style={{ marginBottom: 0, marginTop: 0 }}
value={values.cluster_size}
>
Expand All @@ -622,6 +645,7 @@ const DatabaseCreate = () => {
</Grid>
<Divider spacingBottom={12} spacingTop={26} />
<DatabaseCreateAccessControls
disabled={isRestricted}
errors={ipErrorsFromAPI}
ips={values.allow_list}
onBlur={handleIPBlur}
Expand All @@ -636,6 +660,7 @@ const DatabaseCreate = () => {
<Button
buttonType="primary"
className={classes.createBtn}
disabled={isRestricted}
loading={isSubmitting}
type="submit"
>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { APIError } from '@linode/api-v4/lib/types';
import { Theme } from '@mui/material/styles';
import Grid from '@mui/material/Unstable_Grid2';
import * as React from 'react';
import { ChangeEvent, useState } from 'react';
import { makeStyles } from 'tss-react/mui';

import { FormControlLabel } from 'src/components/FormControlLabel';
import { Link } from 'src/components/Link';
import { MultipleIPInput } from 'src/components/MultipleIPInput/MultipleIPInput';
Expand All @@ -11,36 +12,39 @@ import { Radio } from 'src/components/Radio/Radio';
import { RadioGroup } from 'src/components/RadioGroup';
import { Typography } from 'src/components/Typography';
import { ExtendedIP, ipFieldPlaceholder } from 'src/utilities/ipUtils';
import { makeStyles } from 'tss-react/mui';

import { useIsDatabasesEnabled } from '../utilities';

import type { APIError } from '@linode/api-v4/lib/types';

const useStyles = makeStyles()((theme: Theme) => ({
header: {
marginBottom: theme.spacing(0.5),
},
subHeader: {
marginTop: theme.spacing(2),
},
container: {
marginTop: theme.spacing(3),
maxWidth: 450,
},
header: {
marginBottom: theme.spacing(0.5),
},
multipleIPInput: {
marginLeft: theme.spacing(4),
},
subHeader: {
marginTop: theme.spacing(2),
},
}));

export type AccessOption = 'specific' | 'none';

interface Props {
disabled?: boolean;
errors?: APIError[];
ips: ExtendedIP[];
onBlur: (ips: ExtendedIP[]) => void;
onChange: (ips: ExtendedIP[]) => void;
}

export const DatabaseCreateAccessControls = (props: Props) => {
const { errors, ips, onBlur, onChange } = props;
const { disabled = false, errors, ips, onBlur, onChange } = props;
const { classes } = useStyles();
const [accessOption, setAccessOption] = useState<AccessOption>('specific');
const { isDatabasesV2GA } = useIsDatabasesEnabled();
Expand Down Expand Up @@ -110,12 +114,13 @@ export const DatabaseCreateAccessControls = (props: Props) => {
<FormControlLabel
control={<Radio />}
data-qa-dbaas-radio="Specific"
disabled={disabled}
label="Specific Access (recommended)"
value="specific"
/>
<MultipleIPInput
className={classes.multipleIPInput}
disabled={accessOption === 'none'}
disabled={accessOption === 'none' || disabled}
ips={ips}
onBlur={onBlur}
onChange={onChange}
Expand All @@ -125,12 +130,14 @@ export const DatabaseCreateAccessControls = (props: Props) => {
<FormControlLabel
control={<Radio />}
data-qa-dbaas-radio="None"
disabled={disabled}
label="No Access (Deny connections from all IP addresses)"
value="none"
/>
</RadioGroup>
) : (
<MultipleIPInput
disabled={disabled}
ips={ips}
onBlur={onBlur}
onChange={onChange}
Expand Down

0 comments on commit b5da866

Please sign in to comment.