Skip to content

Commit

Permalink
regression: missing permission checks on voip for team collab (#33518)
Browse files Browse the repository at this point in the history
  • Loading branch information
aleksandernsilva authored Oct 15, 2024
1 parent 31eb47f commit 5cc9ac5
Show file tree
Hide file tree
Showing 14 changed files with 159 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import useVideoConfMenuOptions from './useVideoConfMenuOptions';
import useVoipMenuOptions from './useVoipMenuOptions';

export const useStartCallRoomAction = () => {
const voipCall = useVideoConfMenuOptions();
const videoCall = useVoipMenuOptions();
const videoCall = useVideoConfMenuOptions();
const voipCall = useVoipMenuOptions();

return useMemo((): RoomToolboxActionConfig | undefined => {
if (!videoCall.allowed && !voipCall.allowed) {
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/client/views/admin/users/AdminUsersPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,12 @@ const AdminUsersPage = (): ReactElement => {
<UsersTable
filteredUsersQueryResult={filteredUsersQueryResult}
setUserFilters={setUserFilters}
onReload={handleReload}
paginationData={paginationData}
sortData={sortData}
tab={tab}
isSeatsCapExceeded={isSeatsCapExceeded}
roleData={data}
onReload={handleReload}
/>
</PageContent>
</Page>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,31 @@ import '@testing-library/jest-dom';

import UsersPageHeaderContent from './UsersPageHeaderContent';

it('should render "Associate Extension" button when VoIP_TeamCollab_Enabled setting is enabled', async () => {
it('should not show "Assign Extension" button if voip setting is enabled but user dont have required permission', async () => {
render(<UsersPageHeaderContent isSeatsCapExceeded={false} seatsCap={{ activeUsers: 1, maxActiveUsers: 1 }} />, {
legacyRoot: true,
wrapper: mockAppRoot().withJohnDoe().withSetting('VoIP_TeamCollab_Enabled', true).build(),
});

expect(screen.queryByRole('button', { name: 'Assign_extension' })).not.toBeInTheDocument();
});

it('should not show "Assign Extension" button if user has required permission but voip setting is disabled', async () => {
render(<UsersPageHeaderContent isSeatsCapExceeded={false} seatsCap={{ activeUsers: 1, maxActiveUsers: 1 }} />, {
legacyRoot: true,
wrapper: mockAppRoot().withJohnDoe().withSetting('VoIP_TeamCollab_Enabled', true).build(),
});

expect(screen.queryByRole('button', { name: 'Assign_extension' })).not.toBeInTheDocument();
});

it('should show "Assign Extension" button if user has required permission and voip setting is enabled', async () => {
render(<UsersPageHeaderContent isSeatsCapExceeded={false} seatsCap={{ activeUsers: 1, maxActiveUsers: 1 }} />, {
legacyRoot: true,
wrapper: mockAppRoot().withJohnDoe().withSetting('VoIP_TeamCollab_Enabled', true).withPermission('manage-voip-extensions').build(),
});

expect(screen.getByRole('button', { name: 'Assign_extension' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Assign_extension' })).toBeEnabled();
});

Expand Down
14 changes: 5 additions & 9 deletions apps/meteor/client/views/admin/users/UsersPageHeaderContent.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { Button, ButtonGroup, Margins } from '@rocket.chat/fuselage';
import { usePermission, useRouter, useSetModal, useSetting } from '@rocket.chat/ui-contexts';
import { usePermission, useRouter } from '@rocket.chat/ui-contexts';
import React from 'react';
import { useTranslation } from 'react-i18next';

import { useExternalLink } from '../../../hooks/useExternalLink';
import { useCheckoutUrl } from '../subscription/hooks/useCheckoutUrl';
import SeatsCapUsage from './SeatsCapUsage';
import type { SeatCapProps } from './useSeatsCap';
import AssignExtensionModal from './voip/AssignExtensionModal';
import AssignExtensionButton from './voip/AssignExtensionButton';
import { useVoipExtensionPermission } from './voip/hooks/useVoipExtensionPermission';

type UsersPageHeaderContentProps = {
isSeatsCapExceeded: boolean;
Expand All @@ -17,10 +18,9 @@ type UsersPageHeaderContentProps = {
const UsersPageHeaderContent = ({ isSeatsCapExceeded, seatsCap }: UsersPageHeaderContentProps) => {
const { t } = useTranslation();
const router = useRouter();
const setModal = useSetModal();
const canCreateUser = usePermission('create-user');
const canBulkCreateUser = usePermission('bulk-register-user');
const canRegisterExtension = useSetting('VoIP_TeamCollab_Enabled');
const canManageVoipExtension = useVoipExtensionPermission();

const manageSubscriptionUrl = useCheckoutUrl()({ target: 'user-page', action: 'buy_more' });
const openExternalLink = useExternalLink();
Expand All @@ -41,11 +41,7 @@ const UsersPageHeaderContent = ({ isSeatsCapExceeded, seatsCap }: UsersPageHeade
</Margins>
)}
<ButtonGroup>
{canRegisterExtension && (
<Button icon='phone' onClick={(): void => setModal(<AssignExtensionModal onClose={(): void => setModal(null)} />)}>
{t('Assign_extension')}
</Button>
)}
{canManageVoipExtension && <AssignExtensionButton />}

{canBulkCreateUser && (
<Button icon='mail' onClick={handleInviteButtonClick} disabled={isSeatsCapExceeded}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const createFakeAdminUser = (freeSwitchExtension?: string) =>
freeSwitchExtension,
});

it('should not render "Voice call extension" column when voice call is disabled', async () => {
it('should not render voip extension column when voice call is disabled', async () => {
const user = createFakeAdminUser('1000');

render(
Expand Down Expand Up @@ -41,7 +41,7 @@ it('should not render "Voice call extension" column when voice call is disabled'
expect(screen.queryByRole('option', { name: /Unassign_extension/ })).not.toBeInTheDocument();
});

it('should render "Unassign_extension" button when user has a associated extension', async () => {
it('should not render voip extension column or actions if user doesnt have the required permission', async () => {
const user = createFakeAdminUser('1000');

render(
Expand All @@ -61,6 +61,34 @@ it('should render "Unassign_extension" button when user has a associated extensi
},
);

expect(screen.queryByText('Voice_call_extension')).not.toBeInTheDocument();

screen.getByRole('button', { name: 'More_actions' }).click();
expect(await screen.findByRole('listbox')).toBeInTheDocument();
expect(screen.queryByRole('option', { name: /Assign_extension/ })).not.toBeInTheDocument();
expect(screen.queryByRole('option', { name: /Unassign_extension/ })).not.toBeInTheDocument();
});

it('should render "Unassign_extension" button when user has a associated extension', async () => {
const user = createFakeAdminUser('1000');

render(
<UsersTable
filteredUsersQueryResult={{ isSuccess: true, data: { users: [user], count: 1, offset: 1, total: 1 } } as any}
setUserFilters={() => undefined}
tab='all'
onReload={() => undefined}
paginationData={{} as any}
sortData={{} as any}
isSeatsCapExceeded={false}
roleData={undefined}
/>,
{
legacyRoot: true,
wrapper: mockAppRoot().withUser(user).withSetting('VoIP_TeamCollab_Enabled', true).withPermission('manage-voip-extensions').build(),
},
);

expect(screen.getByText('Voice_call_extension')).toBeInTheDocument();

screen.getByRole('button', { name: 'More_actions' }).click();
Expand All @@ -85,7 +113,7 @@ it('should render "Assign_extension" button when user has no associated extensio
/>,
{
legacyRoot: true,
wrapper: mockAppRoot().withUser(user).withSetting('VoIP_TeamCollab_Enabled', true).build(),
wrapper: mockAppRoot().withUser(user).withSetting('VoIP_TeamCollab_Enabled', true).withPermission('manage-voip-extensions').build(),
},
);

Expand Down
20 changes: 11 additions & 9 deletions apps/meteor/client/views/admin/users/UsersTable/UsersTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Pagination } from '@rocket.chat/fuselage';
import { useEffectEvent, useBreakpoints } from '@rocket.chat/fuselage-hooks';
import type { PaginatedResult, DefaultUserInfo } from '@rocket.chat/rest-typings';
import type { TranslationKey } from '@rocket.chat/ui-contexts';
import { useRouter, useTranslation, useSetting } from '@rocket.chat/ui-contexts';
import { useRouter, useTranslation } from '@rocket.chat/ui-contexts';
import type { UseQueryResult } from '@tanstack/react-query';
import type { ReactElement, Dispatch, SetStateAction } from 'react';
import React, { useMemo } from 'react';
Expand All @@ -19,6 +19,7 @@ import {
import type { usePagination } from '../../../../components/GenericTable/hooks/usePagination';
import type { useSort } from '../../../../components/GenericTable/hooks/useSort';
import type { AdminUsersTab, UsersFilters, UsersTableSortingOption } from '../AdminUsersPage';
import { useVoipExtensionPermission } from '../voip/hooks/useVoipExtensionPermission';
import UsersTableFilters from './UsersTableFilters';
import UsersTableRow from './UsersTableRow';

Expand Down Expand Up @@ -49,13 +50,14 @@ const UsersTable = ({

const isMobile = !breakpoints.includes('xl');
const isLaptop = !breakpoints.includes('xxl');
const isVoIPEnabled = useSetting<boolean>('VoIP_TeamCollab_Enabled') || false;

const { data, isLoading, isError, isSuccess } = filteredUsersQueryResult;

const { current, itemsPerPage, setItemsPerPage, setCurrent, ...paginationProps } = paginationData;
const { sortBy, sortDirection, setSort } = sortData;

const canManageVoipExtension = useVoipExtensionPermission();

const isKeyboardEvent = (
event: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>,
): event is React.KeyboardEvent<HTMLElement> => {
Expand Down Expand Up @@ -112,7 +114,7 @@ const UsersTable = ({
{t('Pending_action')}
</GenericTableHeaderCell>
),
tab === 'all' && isVoIPEnabled && (
tab === 'all' && canManageVoipExtension && (
<GenericTableHeaderCell
w='x180'
key='freeSwitchExtension'
Expand All @@ -126,7 +128,7 @@ const UsersTable = ({
),
<GenericTableHeaderCell key='actions' w={tab === 'pending' ? 'x204' : 'x50'} />,
],
[isLaptop, isMobile, setSort, sortBy, sortDirection, t, tab, isVoIPEnabled],
[isLaptop, isMobile, setSort, sortBy, sortDirection, t, tab, canManageVoipExtension],
);

return (
Expand Down Expand Up @@ -162,14 +164,14 @@ const UsersTable = ({
{data.users.map((user) => (
<UsersTableRow
key={user._id}
onClick={handleClickOrKeyDown}
tab={tab}
user={user}
isMobile={isMobile}
isLaptop={isLaptop}
user={user}
onReload={onReload}
tab={tab}
isSeatsCapExceeded={isSeatsCapExceeded}
showVoipExtension={isVoIPEnabled}
showVoipExtension={canManageVoipExtension}
onReload={onReload}
onClick={handleClickOrKeyDown}
/>
))}
</GenericTableBody>
Expand Down
21 changes: 13 additions & 8 deletions apps/meteor/client/views/admin/users/UsersTable/UsersTableRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,28 @@ import { useDeleteUserAction } from '../hooks/useDeleteUserAction';
import { useResetE2EEKeyAction } from '../hooks/useResetE2EEKeyAction';
import { useResetTOTPAction } from '../hooks/useResetTOTPAction';
import { useSendWelcomeEmailMutation } from '../hooks/useSendWelcomeEmailMutation';
import { useVoipExtensionAction } from '../hooks/useVoipExtensionAction';
import { useVoipExtensionAction } from '../voip/hooks/useVoipExtensionAction';

type UsersTableRowProps = {
user: Serialized<DefaultUserInfo>;
onClick: (id: IUser['_id'], e: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>) => void;
tab: AdminUsersTab;
isMobile: boolean;
isLaptop: boolean;
onReload: () => void;
tab: AdminUsersTab;
onClick: (id: IUser['_id'], e: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>) => void;
isSeatsCapExceeded: boolean;
showVoipExtension: boolean;
};

const UsersTableRow = ({
user,
onClick,
onReload,
tab,
isMobile,
isLaptop,
tab,
isSeatsCapExceeded,
showVoipExtension,
onClick,
onReload,
}: UsersTableRowProps): ReactElement => {
const t = useTranslation();

Expand Down Expand Up @@ -75,7 +75,12 @@ const UsersTableRow = ({
const resetTOTPAction = useResetTOTPAction(userId);
const resetE2EKeyAction = useResetE2EEKeyAction(userId);
const resendWelcomeEmail = useSendWelcomeEmailMutation();
const voipExtensionAction = useVoipExtensionAction({ extension: freeSwitchExtension, username, name });
const voipExtensionAction = useVoipExtensionAction({
enabled: showVoipExtension,
extension: freeSwitchExtension,
username,
name,
});

const isNotPendingDeactivatedNorFederated = tab !== 'pending' && tab !== 'deactivated' && !isFederatedUser;
const menuOptions = useMemo(
Expand Down Expand Up @@ -173,7 +178,7 @@ const UsersTableRow = ({
</GenericTableCell>
)}

{tab === 'all' && showVoipExtension && username && (
{tab === 'all' && showVoipExtension && (
<GenericTableCell fontScale='p2' color='hint' withTruncatedText>
{freeSwitchExtension || t('Not_assigned')}
</GenericTableCell>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
import { useSetModal, useSetting } from '@rocket.chat/ui-contexts';
import { useSetModal } from '@rocket.chat/ui-contexts';
import React from 'react';
import { useTranslation } from 'react-i18next';

import type { Action } from '../../../hooks/useActionSpread';
import AssignExtensionModal from '../voip/AssignExtensionModal';
import RemoveExtensionModal from '../voip/RemoveExtensionModal';
import { useVoipExtensionPermission } from '../voip/hooks/useVoipExtensionPermission';

type VoipExtensionActionParams = {
name: string;
Expand All @@ -14,7 +15,7 @@ type VoipExtensionActionParams = {
};

export const useVoipExtensionAction = ({ name, username, extension }: VoipExtensionActionParams): Action | undefined => {
const isVoipEnabled = useSetting('VoIP_TeamCollab_Enabled');
const canManageVoipExtensions = useVoipExtensionPermission();
const { t } = useTranslation();
const setModal = useSetModal();

Expand All @@ -27,7 +28,7 @@ export const useVoipExtensionAction = ({ name, username, extension }: VoipExtens
setModal(<AssignExtensionModal defaultUsername={username} onClose={(): void => setModal(null)} />);
});

return isVoipEnabled
return canManageVoipExtensions
? {
icon: extension ? 'phone-disabled' : 'phone',
label: extension ? t('Unassign_extension') : t('Assign_extension'),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
import { IconButton } from '@rocket.chat/fuselage';
import { Button } from '@rocket.chat/fuselage';
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
import { useSetModal } from '@rocket.chat/ui-contexts';
import React from 'react';
import { useTranslation } from 'react-i18next';

import { GenericTableCell } from '../../../../components/GenericTable';
import AssignExtensionModal from './AssignExtensionModal';

const AssignExtensionButton = ({ username }: { username: string }) => {
const AssignExtensionButton = () => {
const { t } = useTranslation();
const setModal = useSetModal();

const handleAssociation = useEffectEvent((e) => {
e.stopPropagation();
setModal(<AssignExtensionModal defaultUsername={username} onClose={(): void => setModal(null)} />);
const handleAssign = useEffectEvent(() => {
setModal(<AssignExtensionModal onClose={(): void => setModal(null)} />);
});

return (
<GenericTableCell fontScale='p2' color='hint' withTruncatedText>
<IconButton icon='user-plus' small title={t('Associate_Extension')} onClick={handleAssociation} />
</GenericTableCell>
<Button icon='phone' onClick={handleAssign}>
{t('Assign_extension')}
</Button>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
import { useSetModal } from '@rocket.chat/ui-contexts';
import React from 'react';
import { useTranslation } from 'react-i18next';

import type { Action } from '../../../../hooks/useActionSpread';
import AssignExtensionModal from '../AssignExtensionModal';
import RemoveExtensionModal from '../RemoveExtensionModal';

type VoipExtensionActionParams = {
name: string;
username: string;
extension?: string;
enabled: boolean;
};

export const useVoipExtensionAction = ({ name, username, extension, enabled }: VoipExtensionActionParams): Action | undefined => {
const { t } = useTranslation();
const setModal = useSetModal();

const handleExtensionAssignment = useEffectEvent(() => {
if (extension) {
setModal(<RemoveExtensionModal name={name} username={username} extension={extension} onClose={(): void => setModal(null)} />);
return;
}

setModal(<AssignExtensionModal defaultUsername={username} onClose={(): void => setModal(null)} />);
});

return enabled
? {
icon: extension ? 'phone-disabled' : 'phone',
label: extension ? t('Unassign_extension') : t('Assign_extension'),
action: handleExtensionAssignment,
}
: undefined;
};
Loading

0 comments on commit 5cc9ac5

Please sign in to comment.