Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

upcoming, change: [M3-7524] - Add child_account OAuth scope to Create and View PAT drawers #9992

Merged
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Add `child_account` oauth scope to Personal Access Token drawers ([#9992](https://github.com/linode/manager/pull/9992))
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,25 @@ import userEvent from '@testing-library/user-event';
import * as React from 'react';

import { appTokenFactory } from 'src/factories';
import { accountUserFactory } from 'src/factories/accountUsers';
import { rest, server } from 'src/mocks/testServer';
import { renderWithTheme } from 'src/utilities/testHelpers';

import { CreateAPITokenDrawer } from './CreateAPITokenDrawer';

// Mock the useAccountUser hooks to immediately return the expected data, circumventing the HTTP request and loading state.
const queryMocks = vi.hoisted(() => ({
useAccountUser: vi.fn().mockReturnValue({}),
}));

vi.mock('src/queries/accountUsers', async () => {
const actual = await vi.importActual<any>('src/queries/accountUsers');
return {
...actual,
useAccountUser: queryMocks.useAccountUser,
};
});

const props = {
onClose: vi.fn(),
open: true,
Expand Down Expand Up @@ -38,6 +52,7 @@ describe('Create API Token Drawer', () => {
expect(cancelBtn).toBeEnabled();
expect(cancelBtn).toBeVisible();
});

it('Should see secret modal with secret when you type a label and submit the form successfully', async () => {
server.use(
rest.post('*/profile/tokens', (req, res, ctx) => {
Expand All @@ -58,19 +73,60 @@ describe('Create API Token Drawer', () => {
expect(props.showSecret).toBeCalledWith('secret-value')
);
});

// TODO: Parent/Child - remove this test when Parent/Child feature is released.
it('Should default to read/write for all scopes', () => {
const { getByLabelText } = renderWithTheme(
<CreateAPITokenDrawer {...props} />
);
const selectAllReadWriteRadioButton = getByLabelText(
const selectAllReadWritePermRadioButton = getByLabelText(
'Select read/write for all'
);
expect(selectAllReadWriteRadioButton).toBeChecked();
expect(selectAllReadWritePermRadioButton).toBeChecked();
});

it('Should default to None for all scopes with the parent/child feature flag on', () => {
const { getByLabelText } = renderWithTheme(
<CreateAPITokenDrawer {...props} />,
{ flags: { parentChildAccountAccess: true } }
);
const selectAllNonePermRadioButton = getByLabelText('Select none for all');
expect(selectAllNonePermRadioButton).toBeChecked();
});

it('Should default to 6 months for expiration', () => {
const { getByText } = renderWithTheme(<CreateAPITokenDrawer {...props} />);
getByText('In 6 months');
});

it('Should show the Child Account Access scope for a parent user account with the parent/child feature flag on', () => {
queryMocks.useAccountUser.mockReturnValue({
data: accountUserFactory.build({ user_type: 'parent' }),
});

const { getByText } = renderWithTheme(<CreateAPITokenDrawer {...props} />, {
flags: { parentChildAccountAccess: true },
});
const childScope = getByText('Child Account Access');
expect(childScope).toBeInTheDocument();
});

it('Should not show the Child Account Access scope for a non-parent user account with the parent/child feature flag on', () => {
queryMocks.useAccountUser.mockReturnValue({
data: accountUserFactory.build({ user_type: null }),
});

const { queryByText } = renderWithTheme(
<CreateAPITokenDrawer {...props} />,
{
flags: { parentChildAccountAccess: true },
}
);

const childScope = queryByText('Child Account Access');
expect(childScope).not.toBeInTheDocument();
});

it('Should close when Cancel is pressed', () => {
const { getByText } = renderWithTheme(<CreateAPITokenDrawer {...props} />);
const cancelButton = getByText(/Cancel/);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@ import * as React from 'react';
import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel';
import { Drawer } from 'src/components/Drawer';
import Select, { Item } from 'src/components/EnhancedSelect/Select';
import { FormControl } from 'src/components/FormControl';
import { FormHelperText } from 'src/components/FormHelperText';
import { Notice } from 'src/components/Notice/Notice';
import { Radio } from 'src/components/Radio/Radio';
import { TableBody } from 'src/components/TableBody';
import { TableCell } from 'src/components/TableCell';
import { TableHead } from 'src/components/TableHead';
import { TableRow } from 'src/components/TableRow';
import { TextField } from 'src/components/TextField';
import { FormControl } from 'src/components/FormControl';
import { FormHelperText } from 'src/components/FormHelperText';
import { ISO_DATETIME_NO_TZ_FORMAT } from 'src/constants';
import { AccessCell } from 'src/features/ObjectStorage/AccessKeyLanding/AccessCell';
import { useFlags } from 'src/hooks/useFlags';
import { useAccountUser } from 'src/queries/accountUsers';
import { useProfile } from 'src/queries/profile';
import { useCreatePersonalAccessTokenMutation } from 'src/queries/tokens';
import { getErrorMap } from 'src/utilities/errorUtils';

Expand Down Expand Up @@ -82,12 +85,17 @@ export const CreateAPITokenDrawer = (props: Props) => {
const expiryTups = genExpiryTups();
const { onClose, open, showSecret } = props;

const flags = useFlags();

const initialValues = {
expiry: expiryTups[0][1],
label: '',
scopes: scopeStringToPermTuples('*'),
scopes: scopeStringToPermTuples(flags.parentChildAccountAccess ? '' : '*'),
mjac0bs marked this conversation as resolved.
Show resolved Hide resolved
};

const { data: profile } = useProfile();
const { data: user } = useAccountUser(profile?.username ?? '');

const {
error,
isLoading,
Expand Down Expand Up @@ -241,50 +249,58 @@ export const CreateAPITokenDrawer = (props: Props) => {
return null;
}
return (
<TableRow
data-qa-row={basePermNameMap[scopeTup[0]]}
key={scopeTup[0]}
>
<StyledAccessCell padding="checkbox" parentColumn="Access">
{basePermNameMap[scopeTup[0]]}
</StyledAccessCell>
<StyledPermissionsCell padding="checkbox" parentColumn="None">
<AccessCell
active={scopeTup[1] === 0}
disabled={false}
onChange={handleScopeChange}
scope="0"
scopeDisplay={scopeTup[0]}
viewOnly={false}
/>
</StyledPermissionsCell>
<StyledPermissionsCell
padding="checkbox"
parentColumn="Read Only"
>
<AccessCell
active={scopeTup[1] === 1}
disabled={false}
onChange={handleScopeChange}
scope="1"
scopeDisplay={scopeTup[0]}
viewOnly={false}
/>
</StyledPermissionsCell>
<StyledPermissionsCell
padding="checkbox"
parentColumn="Read/Write"
// When the feature flag is on, display the Child Account Access scope for parent user accounts only.
(!flags.parentChildAccountAccess &&
basePermNameMap[scopeTup[0]] === 'Child Account Access') ||
(flags.parentChildAccountAccess &&
user?.user_type !== 'parent' &&
basePermNameMap[scopeTup[0]] ===
'Child Account Access') ? null : (
<TableRow
data-qa-row={basePermNameMap[scopeTup[0]]}
key={scopeTup[0]}
>
mjac0bs marked this conversation as resolved.
Show resolved Hide resolved
<AccessCell
active={scopeTup[1] === 2}
disabled={false}
onChange={handleScopeChange}
scope="2"
scopeDisplay={scopeTup[0]}
viewOnly={false}
/>
</StyledPermissionsCell>
</TableRow>
<StyledAccessCell padding="checkbox" parentColumn="Access">
{basePermNameMap[scopeTup[0]]}
</StyledAccessCell>
<StyledPermissionsCell padding="checkbox" parentColumn="None">
<AccessCell
active={scopeTup[1] === 0}
disabled={false}
onChange={handleScopeChange}
scope="0"
scopeDisplay={scopeTup[0]}
viewOnly={false}
/>
</StyledPermissionsCell>
<StyledPermissionsCell
padding="checkbox"
parentColumn="Read Only"
>
<AccessCell
active={scopeTup[1] === 1}
disabled={false}
onChange={handleScopeChange}
scope="1"
scopeDisplay={scopeTup[0]}
viewOnly={false}
/>
</StyledPermissionsCell>
<StyledPermissionsCell
padding="checkbox"
parentColumn="Read/Write"
>
<AccessCell
active={scopeTup[1] === 2}
disabled={false}
onChange={handleScopeChange}
scope="2"
scopeDisplay={scopeTup[0]}
viewOnly={false}
/>
</StyledPermissionsCell>
</TableRow>
)
);
})}
</TableBody>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,32 @@
import * as React from 'react';

import { appTokenFactory } from 'src/factories';
import { accountUserFactory } from 'src/factories/accountUsers';
import { renderWithTheme } from 'src/utilities/testHelpers';

import { ViewAPITokenDrawer } from './ViewAPITokenDrawer';
import { basePerms } from './utils';

// Mock the useAccountUser hooks to immediately return the expected data, circumventing the HTTP request and loading state.
const queryMocks = vi.hoisted(() => ({
useAccountUser: vi.fn().mockReturnValue({}),
}));

vi.mock('src/queries/accountUsers', async () => {
const actual = await vi.importActual<any>('src/queries/accountUsers');
return {
...actual,
useAccountUser: queryMocks.useAccountUser,
};
});

const nonParentPerms = basePerms.filter((value) => value !== 'child_account');

const token = appTokenFactory.build({ label: 'my-token', scopes: '*' });
const limitedToken = appTokenFactory.build({
label: 'my-limited-token',
scopes: '',
});

const props = {
onClose: vi.fn(),
Expand All @@ -21,24 +41,37 @@ describe('View API Token Drawer', () => {
expect(getByText(token.label)).toBeVisible();
});

it('should all permissions as read/write with wildcard scopes', () => {
it('should show all permissions as read/write with wildcard scopes', () => {
const { getByTestId } = renderWithTheme(<ViewAPITokenDrawer {...props} />);
for (const permissionName of basePerms) {
for (const permissionName of nonParentPerms) {
expect(getByTestId(`perm-${permissionName}`)).toHaveAttribute(
'aria-label',
`This token has 2 access for ${permissionName}`
);
}
});

it('should show all permissions as none with no scopes', () => {
const { getByTestId } = renderWithTheme(
<ViewAPITokenDrawer {...props} token={limitedToken} />,
{ flags: { parentChildAccountAccess: false } }
);
for (const permissionName of nonParentPerms) {
expect(getByTestId(`perm-${permissionName}`)).toHaveAttribute(
'aria-label',
`This token has 0 access for ${permissionName}`
);
}
});

it('only account has read/write, all others are none', () => {
const { getByTestId } = renderWithTheme(
<ViewAPITokenDrawer
{...props}
token={appTokenFactory.build({ scopes: 'account:read_write' })}
/>
);
for (const permissionName of basePerms) {
for (const permissionName of nonParentPerms) {
// We only expect account to have read/write for this test
const expectedScopeLevel = permissionName === 'account' ? 2 : 0;
expect(getByTestId(`perm-${permissionName}`)).toHaveAttribute(
Expand Down Expand Up @@ -76,12 +109,55 @@ describe('View API Token Drawer', () => {
volumes: 1,
} as const;

for (const permissionName of basePerms) {
for (const permissionName of nonParentPerms) {
const expectedScopeLevel = expectedScopeLevels[permissionName];
expect(getByTestId(`perm-${permissionName}`)).toHaveAttribute(
'aria-label',
`This token has ${expectedScopeLevel} access for ${permissionName}`
);
}
});

it('should show Child Account Access scope with read/write perms for a parent user account with the parent/child feature flag on', () => {
queryMocks.useAccountUser.mockReturnValue({
data: accountUserFactory.build({ user_type: 'parent' }),
});

const { getByTestId, getByText } = renderWithTheme(
<ViewAPITokenDrawer
{...props}
token={appTokenFactory.build({
scopes: 'child_account:read_write',
})}
/>,
{
flags: { parentChildAccountAccess: true },
}
);

const childScope = getByText('Child Account Access');
const expectedScopeLevels = {
child_account: 2,
} as const;
const childPermissionName = 'child_account';

expect(childScope).toBeInTheDocument();
expect(getByTestId(`perm-${childPermissionName}`)).toHaveAttribute(
'aria-label',
`This token has ${expectedScopeLevels[childPermissionName]} access for ${childPermissionName}`
);
});

it('should not show the Child Account Access scope for a non-parent user account with the parent/child feature flag on', () => {
queryMocks.useAccountUser.mockReturnValue({
data: accountUserFactory.build({ user_type: null }),
});

const { queryByText } = renderWithTheme(<ViewAPITokenDrawer {...props} />, {
flags: { parentChildAccountAccess: true },
});

const childScope = queryByText('Child Account Access');
expect(childScope).not.toBeInTheDocument();
});
});
Loading