Skip to content

Commit

Permalink
upcoming: [M3-7481] - Disable creating and editing API tokens for pro…
Browse files Browse the repository at this point in the history
…xy users (#10109)

* Disable creating and editing API tokens for proxy users

* Added changeset: Disable adding and editing API tokens for proxy users

* A feature flag would be nice

* Add Cypress test coverage

* Fix test comments

* Add mocked feature flag to test

* Address feedback: use tooltip ui helper in test
  • Loading branch information
mjac0bs authored Jan 25, 2024
1 parent 9c102cc commit f0b774e
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Disable adding and editing API tokens for proxy users ([#10109](https://github.com/linode/manager/pull/10109))
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,22 @@

import { Token } from '@linode/api-v4/types';
import { appTokenFactory } from 'src/factories/oauth';
import { profileFactory } from 'src/factories/profile';
import {
mockCreatePersonalAccessToken,
mockGetAppTokens,
mockGetPersonalAccessTokens,
mockGetProfile,
mockRevokePersonalAccessToken,
mockUpdatePersonalAccessToken,
} from 'support/intercepts/profile';
import { randomLabel, randomString } from 'support/util/random';
import { ui } from 'support/ui';
import {
mockAppendFeatureFlags,
mockGetFeatureFlagClientstream,
} from 'support/intercepts/feature-flags';
import { makeFeatureFlagData } from 'support/util/feature-flags';

describe('Personal access tokens', () => {
/*
Expand Down Expand Up @@ -224,4 +231,99 @@ describe('Personal access tokens', () => {
cy.findByText('No items to display.').should('be.visible');
});
});

/*
* - Uses mocked API requests to confirm disabled states for proxy users
* - Confirms that a proxy user cannot create an API token
* - Confirms that a proxy user cannot edit (rename) an API token
* - Confirms that a proxy user can revoke an API token created for them
* - Confirms that token is removed from list after revoking it
*/
it('disables API token creation and editing for a proxy user', () => {
const proxyToken: Token = appTokenFactory.build({
label: randomLabel(),
token: randomString(64),
});
const proxyUserProfile = profileFactory.build({ user_type: 'proxy' });

// TODO: Parent/Child - M3-7559 clean up when feature is live in prod and feature flag is removed.
mockAppendFeatureFlags({
parentChildAccountAccess: makeFeatureFlagData(true),
}).as('getFeatureFlags');
mockGetFeatureFlagClientstream().as('getClientStream');

mockGetProfile(proxyUserProfile);
mockGetPersonalAccessTokens([proxyToken]).as('getTokens');
mockGetAppTokens([]).as('getAppTokens');
mockRevokePersonalAccessToken(proxyToken.id).as('revokeToken');

cy.visitWithLogin('/profile/tokens');
cy.wait([
'@getClientStream',
'@getFeatureFlags',
'@getTokens',
'@getAppTokens',
]);

// Find 'Create a Personal Access Token' button, confirm it is disabled and tooltip displays.
ui.button
.findByTitle('Create a Personal Access Token')
.should('be.visible')
.should('be.disabled')
.click();

ui.tooltip
.findByText('You can only create tokens for your own company.')
.should('be.visible');

// Find token in list, confirm "Rename" is disabled and tooltip displays.
cy.findByText(proxyToken.label)
.should('be.visible')
.closest('tr')
.within(() => {
ui.button
.findByTitle('Rename')
.should('be.visible')
.should('be.disabled')
.click();
});

ui.tooltip
.findByText('Only company users can edit API tokens.')
.should('be.visible');

// Confirm that token has not been renamed, initiate revocation.
cy.findByText(proxyToken.label)
.should('be.visible')
.closest('tr')
.within(() => {
ui.button
.findByTitle('Revoke')
.should('be.visible')
.should('be.enabled')
.click();
});

mockGetPersonalAccessTokens([]).as('getTokens');
ui.dialog
.findByTitle(`Revoke ${proxyToken.label}?`)
.should('be.visible')
.within(() => {
ui.buttonGroup
.findButtonByTitle('Revoke')
.should('be.visible')
.should('be.enabled')
.click();
});

// Confirm that token is removed from list after revoking.
cy.wait(['@revokeToken', '@getTokens']);
ui.toast.assertMessage(`Successfully revoked ${proxyToken.label}`);
cy.findByLabelText('List of Personal Access Tokens')
.should('be.visible')
.within(() => {
cy.findByText(proxyToken.label).should('not.exist');
cy.findByText('No items to display.').should('be.visible');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Action, ActionMenu } from 'src/components/ActionMenu/ActionMenu';
import { InlineMenuAction } from 'src/components/InlineMenuAction/InlineMenuAction';

interface Props {
isProxyUser: boolean;
isThirdPartyAccessToken: boolean;
openEditDrawer: (token: Token) => void;
openRevokeDialog: (token: Token, type: string) => void;
Expand All @@ -20,6 +21,7 @@ export const APITokenMenu = (props: Props) => {
const matchesSmDown = useMediaQuery(theme.breakpoints.down('md'));

const {
isProxyUser,
isThirdPartyAccessToken,
openEditDrawer,
openRevokeDialog,
Expand All @@ -37,10 +39,12 @@ export const APITokenMenu = (props: Props) => {
},
!isThirdPartyAccessToken
? {
disabled: isProxyUser,
onClick: () => {
openEditDrawer(token);
},
title: 'Rename',
tooltip: 'Only company users can edit API tokens.',
}
: null,
{
Expand All @@ -65,8 +69,10 @@ export const APITokenMenu = (props: Props) => {
{actions.map((action) => (
<InlineMenuAction
actionText={action.title}
disabled={action.disabled}
key={action.title}
onClick={action.onClick}
tooltip={action.tooltip}
/>
))}
</>
Expand Down
15 changes: 15 additions & 0 deletions packages/manager/src/features/Profile/APITokens/APITokenTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ import { StyledTableSortCell } from 'src/components/TableSortCell/StyledTableSor
import { TableSortCell } from 'src/components/TableSortCell/TableSortCell';
import { Typography } from 'src/components/Typography';
import { SecretTokenDialog } from 'src/features/Profile/SecretTokenDialog/SecretTokenDialog';
import { useFlags } from 'src/hooks/useFlags';
import { useOrder } from 'src/hooks/useOrder';
import { usePagination } from 'src/hooks/usePagination';
import { useProfile } from 'src/queries/profile';
import {
useAppTokensQuery,
usePersonalAccessTokensQuery,
Expand Down Expand Up @@ -53,6 +55,8 @@ const PREFERENCE_KEY = 'api-tokens';
export const APITokenTable = (props: Props) => {
const { title, type } = props;

const flags = useFlags();
const { data: profile } = useProfile();
const { handleOrderChange, order, orderBy } = useOrder(
{
order: 'desc',
Expand All @@ -78,6 +82,10 @@ export const APITokenTable = (props: Props) => {
{ '+order': order, '+order_by': orderBy }
);

const isProxyUser = Boolean(
flags.parentChildAccountAccess && profile?.user_type === 'proxy'
);

const [isCreateOpen, setIsCreateOpen] = React.useState<boolean>(false);
const [isRevokeOpen, setIsRevokeOpen] = React.useState<boolean>(false);
const [isViewOpen, setIsViewOpen] = React.useState<boolean>(false);
Expand Down Expand Up @@ -169,6 +177,7 @@ export const APITokenTable = (props: Props) => {
</TableCell>
<TableCell actionCell>
<APITokenMenu
isProxyUser={isProxyUser}
isThirdPartyAccessToken={title === 'Third Party Access Tokens'}
openEditDrawer={openEditDrawer}
openRevokeDialog={openRevokeDialog}
Expand Down Expand Up @@ -197,6 +206,12 @@ export const APITokenTable = (props: Props) => {
<StyledAddNewWrapper>
{type === 'Personal Access Token' && (
<AddNewLink
disabledReason={
isProxyUser
? 'You can only create tokens for your own company.'
: undefined
}
disabled={isProxyUser}
label="Create a Personal Access Token"
onClick={() => setIsCreateOpen(true)}
/>
Expand Down

0 comments on commit f0b774e

Please sign in to comment.