Device manage - handle sessions that don't support encryption (#9717)
* add handling for unverifiable sessions * test * update types for filtervariation * strict fixes * avoid setting up cross signing in device man tests
This commit is contained in:
parent
6150b86421
commit
888e69f39a
12 changed files with 200 additions and 105 deletions
|
@ -49,7 +49,6 @@ describe("Device manager", () => {
|
||||||
|
|
||||||
cy.get('[data-testid="current-session-section"]').within(() => {
|
cy.get('[data-testid="current-session-section"]').within(() => {
|
||||||
cy.contains('Unverified session').should('exist');
|
cy.contains('Unverified session').should('exist');
|
||||||
cy.get('.mx_DeviceSecurityCard_actions [role="button"]').should('exist');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// current session details opened
|
// current session details opened
|
||||||
|
|
|
@ -32,6 +32,7 @@ const VariationIcon: Record<DeviceSecurityVariation, React.FC<React.SVGProps<SVG
|
||||||
[DeviceSecurityVariation.Inactive]: InactiveIcon,
|
[DeviceSecurityVariation.Inactive]: InactiveIcon,
|
||||||
[DeviceSecurityVariation.Verified]: VerifiedIcon,
|
[DeviceSecurityVariation.Verified]: VerifiedIcon,
|
||||||
[DeviceSecurityVariation.Unverified]: UnverifiedIcon,
|
[DeviceSecurityVariation.Unverified]: UnverifiedIcon,
|
||||||
|
[DeviceSecurityVariation.Unverifiable]: UnverifiedIcon,
|
||||||
};
|
};
|
||||||
|
|
||||||
const DeviceSecurityIcon: React.FC<{ variation: DeviceSecurityVariation }> = ({ variation }) => {
|
const DeviceSecurityIcon: React.FC<{ variation: DeviceSecurityVariation }> = ({ variation }) => {
|
||||||
|
|
|
@ -56,6 +56,26 @@ const securityCardContent: Record<DeviceSecurityVariation, {
|
||||||
</p>
|
</p>
|
||||||
</>,
|
</>,
|
||||||
},
|
},
|
||||||
|
// unverifiable uses single-session case
|
||||||
|
// because it is only ever displayed on a single session detail
|
||||||
|
[DeviceSecurityVariation.Unverifiable]: {
|
||||||
|
title: _t('Unverified session'),
|
||||||
|
description: <>
|
||||||
|
<p>{ _t(`This session doesn't support encryption, so it can't be verified.`) }
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{ _t(
|
||||||
|
`You won't be able to participate in rooms where encryption is enabled when using this session.`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</p><p>
|
||||||
|
{ _t(
|
||||||
|
`For best security and privacy, it is recommended to use Matrix clients that support encryption.`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</>,
|
||||||
|
},
|
||||||
[DeviceSecurityVariation.Inactive]: {
|
[DeviceSecurityVariation.Inactive]: {
|
||||||
title: _t('Inactive sessions'),
|
title: _t('Inactive sessions'),
|
||||||
description: <>
|
description: <>
|
||||||
|
|
|
@ -30,18 +30,33 @@ interface Props {
|
||||||
onVerifyDevice?: () => void;
|
onVerifyDevice?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DeviceVerificationStatusCard: React.FC<Props> = ({
|
const getCardProps = (device: ExtendedDevice): {
|
||||||
device,
|
variation: DeviceSecurityVariation;
|
||||||
onVerifyDevice,
|
heading: string;
|
||||||
}) => {
|
description: React.ReactNode;
|
||||||
const securityCardProps = device.isVerified ? {
|
} => {
|
||||||
|
if (device.isVerified) {
|
||||||
|
return {
|
||||||
variation: DeviceSecurityVariation.Verified,
|
variation: DeviceSecurityVariation.Verified,
|
||||||
heading: _t('Verified session'),
|
heading: _t('Verified session'),
|
||||||
description: <>
|
description: <>
|
||||||
{ _t('This session is ready for secure messaging.') }
|
{ _t('This session is ready for secure messaging.') }
|
||||||
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Verified} />
|
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Verified} />
|
||||||
</>,
|
</>,
|
||||||
} : {
|
};
|
||||||
|
}
|
||||||
|
if (device.isVerified === null) {
|
||||||
|
return {
|
||||||
|
variation: DeviceSecurityVariation.Unverified,
|
||||||
|
heading: _t('Unverified session'),
|
||||||
|
description: <>
|
||||||
|
{ _t(`This session doesn't support encryption and thus can't be verified.`) }
|
||||||
|
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Unverifiable} />
|
||||||
|
</>,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
variation: DeviceSecurityVariation.Unverified,
|
variation: DeviceSecurityVariation.Unverified,
|
||||||
heading: _t('Unverified session'),
|
heading: _t('Unverified session'),
|
||||||
description: <>
|
description: <>
|
||||||
|
@ -49,10 +64,19 @@ export const DeviceVerificationStatusCard: React.FC<Props> = ({
|
||||||
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Unverified} />
|
<DeviceSecurityLearnMore variation={DeviceSecurityVariation.Unverified} />
|
||||||
</>,
|
</>,
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DeviceVerificationStatusCard: React.FC<Props> = ({
|
||||||
|
device,
|
||||||
|
onVerifyDevice,
|
||||||
|
}) => {
|
||||||
|
const securityCardProps = getCardProps(device);
|
||||||
|
|
||||||
return <DeviceSecurityCard
|
return <DeviceSecurityCard
|
||||||
{...securityCardProps}
|
{...securityCardProps}
|
||||||
>
|
>
|
||||||
{ !device.isVerified && !!onVerifyDevice &&
|
{ /* check for explicit false to exclude unverifiable devices */ }
|
||||||
|
{ device.isVerified === false && !!onVerifyDevice &&
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
kind='primary'
|
kind='primary'
|
||||||
onClick={onVerifyDevice}
|
onClick={onVerifyDevice}
|
||||||
|
|
|
@ -27,6 +27,7 @@ import { DeviceExpandDetailsButton } from './DeviceExpandDetailsButton';
|
||||||
import DeviceSecurityCard from './DeviceSecurityCard';
|
import DeviceSecurityCard from './DeviceSecurityCard';
|
||||||
import {
|
import {
|
||||||
filterDevicesBySecurityRecommendation,
|
filterDevicesBySecurityRecommendation,
|
||||||
|
FilterVariation,
|
||||||
INACTIVE_DEVICE_AGE_DAYS,
|
INACTIVE_DEVICE_AGE_DAYS,
|
||||||
} from './filter';
|
} from './filter';
|
||||||
import SelectableDeviceTile from './SelectableDeviceTile';
|
import SelectableDeviceTile from './SelectableDeviceTile';
|
||||||
|
@ -47,8 +48,8 @@ interface Props {
|
||||||
expandedDeviceIds: ExtendedDevice['device_id'][];
|
expandedDeviceIds: ExtendedDevice['device_id'][];
|
||||||
signingOutDeviceIds: ExtendedDevice['device_id'][];
|
signingOutDeviceIds: ExtendedDevice['device_id'][];
|
||||||
selectedDeviceIds: ExtendedDevice['device_id'][];
|
selectedDeviceIds: ExtendedDevice['device_id'][];
|
||||||
filter?: DeviceSecurityVariation;
|
filter?: FilterVariation;
|
||||||
onFilterChange: (filter: DeviceSecurityVariation | undefined) => void;
|
onFilterChange: (filter: FilterVariation | undefined) => void;
|
||||||
onDeviceExpandToggle: (deviceId: ExtendedDevice['device_id']) => void;
|
onDeviceExpandToggle: (deviceId: ExtendedDevice['device_id']) => void;
|
||||||
onSignOutDevices: (deviceIds: ExtendedDevice['device_id'][]) => void;
|
onSignOutDevices: (deviceIds: ExtendedDevice['device_id'][]) => void;
|
||||||
saveDeviceName: DevicesState['saveDeviceName'];
|
saveDeviceName: DevicesState['saveDeviceName'];
|
||||||
|
@ -68,12 +69,12 @@ const sortDevicesByLatestActivityThenDisplayName = (left: ExtendedDevice, right:
|
||||||
(right.last_seen_ts || 0) - (left.last_seen_ts || 0)
|
(right.last_seen_ts || 0) - (left.last_seen_ts || 0)
|
||||||
|| ((left.display_name || left.device_id).localeCompare(right.display_name || right.device_id));
|
|| ((left.display_name || left.device_id).localeCompare(right.display_name || right.device_id));
|
||||||
|
|
||||||
const getFilteredSortedDevices = (devices: DevicesDictionary, filter?: DeviceSecurityVariation) =>
|
const getFilteredSortedDevices = (devices: DevicesDictionary, filter?: FilterVariation) =>
|
||||||
filterDevicesBySecurityRecommendation(Object.values(devices), filter ? [filter] : [])
|
filterDevicesBySecurityRecommendation(Object.values(devices), filter ? [filter] : [])
|
||||||
.sort(sortDevicesByLatestActivityThenDisplayName);
|
.sort(sortDevicesByLatestActivityThenDisplayName);
|
||||||
|
|
||||||
const ALL_FILTER_ID = 'ALL';
|
const ALL_FILTER_ID = 'ALL';
|
||||||
type DeviceFilterKey = DeviceSecurityVariation | typeof ALL_FILTER_ID;
|
type DeviceFilterKey = FilterVariation | typeof ALL_FILTER_ID;
|
||||||
|
|
||||||
const securityCardContent: Record<DeviceSecurityVariation, {
|
const securityCardContent: Record<DeviceSecurityVariation, {
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -90,6 +91,12 @@ const securityCardContent: Record<DeviceSecurityVariation, {
|
||||||
`sign out from those you don't recognize or use anymore.`,
|
`sign out from those you don't recognize or use anymore.`,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
[DeviceSecurityVariation.Unverifiable]: {
|
||||||
|
title: _t('Unverified session'),
|
||||||
|
description: _t(
|
||||||
|
`This session doesn't support encryption and thus can't be verified.`,
|
||||||
|
),
|
||||||
|
},
|
||||||
[DeviceSecurityVariation.Inactive]: {
|
[DeviceSecurityVariation.Inactive]: {
|
||||||
title: _t('Inactive sessions'),
|
title: _t('Inactive sessions'),
|
||||||
description: _t(
|
description: _t(
|
||||||
|
@ -100,8 +107,12 @@ const securityCardContent: Record<DeviceSecurityVariation, {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const isSecurityVariation = (filter?: DeviceFilterKey): filter is DeviceSecurityVariation =>
|
const isSecurityVariation = (filter?: DeviceFilterKey): filter is FilterVariation =>
|
||||||
Object.values<string>(DeviceSecurityVariation).includes(filter);
|
!!filter && ([
|
||||||
|
DeviceSecurityVariation.Inactive,
|
||||||
|
DeviceSecurityVariation.Unverified,
|
||||||
|
DeviceSecurityVariation.Verified,
|
||||||
|
] as string[]).includes(filter);
|
||||||
|
|
||||||
const FilterSecurityCard: React.FC<{ filter?: DeviceFilterKey }> = ({ filter }) => {
|
const FilterSecurityCard: React.FC<{ filter?: DeviceFilterKey }> = ({ filter }) => {
|
||||||
if (isSecurityVariation(filter)) {
|
if (isSecurityVariation(filter)) {
|
||||||
|
@ -124,7 +135,7 @@ const FilterSecurityCard: React.FC<{ filter?: DeviceFilterKey }> = ({ filter })
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getNoResultsMessage = (filter?: DeviceSecurityVariation): string => {
|
const getNoResultsMessage = (filter?: FilterVariation): string => {
|
||||||
switch (filter) {
|
switch (filter) {
|
||||||
case DeviceSecurityVariation.Verified:
|
case DeviceSecurityVariation.Verified:
|
||||||
return _t('No verified sessions found.');
|
return _t('No verified sessions found.');
|
||||||
|
@ -136,7 +147,7 @@ const getNoResultsMessage = (filter?: DeviceSecurityVariation): string => {
|
||||||
return _t('No sessions found.');
|
return _t('No sessions found.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
interface NoResultsProps { filter?: DeviceSecurityVariation, clearFilter: () => void}
|
interface NoResultsProps { filter?: FilterVariation, clearFilter: () => void}
|
||||||
const NoResults: React.FC<NoResultsProps> = ({ filter, clearFilter }) =>
|
const NoResults: React.FC<NoResultsProps> = ({ filter, clearFilter }) =>
|
||||||
<div className='mx_FilteredDeviceList_noResults'>
|
<div className='mx_FilteredDeviceList_noResults'>
|
||||||
{ getNoResultsMessage(filter) }
|
{ getNoResultsMessage(filter) }
|
||||||
|
@ -273,7 +284,7 @@ export const FilteredDeviceList =
|
||||||
];
|
];
|
||||||
|
|
||||||
const onFilterOptionChange = (filterId: DeviceFilterKey) => {
|
const onFilterOptionChange = (filterId: DeviceFilterKey) => {
|
||||||
onFilterChange(filterId === ALL_FILTER_ID ? undefined : filterId as DeviceSecurityVariation);
|
onFilterChange(filterId === ALL_FILTER_ID ? undefined : filterId as FilterVariation);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isAllSelected = selectedDeviceIds.length >= sortedDevices.length;
|
const isAllSelected = selectedDeviceIds.length >= sortedDevices.length;
|
||||||
|
|
|
@ -21,7 +21,7 @@ import AccessibleButton from '../../elements/AccessibleButton';
|
||||||
import SettingsSubsection from '../shared/SettingsSubsection';
|
import SettingsSubsection from '../shared/SettingsSubsection';
|
||||||
import DeviceSecurityCard from './DeviceSecurityCard';
|
import DeviceSecurityCard from './DeviceSecurityCard';
|
||||||
import { DeviceSecurityLearnMore } from './DeviceSecurityLearnMore';
|
import { DeviceSecurityLearnMore } from './DeviceSecurityLearnMore';
|
||||||
import { filterDevicesBySecurityRecommendation, INACTIVE_DEVICE_AGE_DAYS } from './filter';
|
import { filterDevicesBySecurityRecommendation, FilterVariation, INACTIVE_DEVICE_AGE_DAYS } from './filter';
|
||||||
import {
|
import {
|
||||||
DeviceSecurityVariation,
|
DeviceSecurityVariation,
|
||||||
ExtendedDevice,
|
ExtendedDevice,
|
||||||
|
@ -31,7 +31,7 @@ import {
|
||||||
interface Props {
|
interface Props {
|
||||||
devices: DevicesDictionary;
|
devices: DevicesDictionary;
|
||||||
currentDeviceId: ExtendedDevice['device_id'];
|
currentDeviceId: ExtendedDevice['device_id'];
|
||||||
goToFilteredList: (filter: DeviceSecurityVariation) => void;
|
goToFilteredList: (filter: FilterVariation) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SecurityRecommendations: React.FC<Props> = ({
|
const SecurityRecommendations: React.FC<Props> = ({
|
||||||
|
|
|
@ -22,10 +22,14 @@ const MS_DAY = 24 * 60 * 60 * 1000;
|
||||||
export const INACTIVE_DEVICE_AGE_MS = 7.776e+9; // 90 days
|
export const INACTIVE_DEVICE_AGE_MS = 7.776e+9; // 90 days
|
||||||
export const INACTIVE_DEVICE_AGE_DAYS = INACTIVE_DEVICE_AGE_MS / MS_DAY;
|
export const INACTIVE_DEVICE_AGE_DAYS = INACTIVE_DEVICE_AGE_MS / MS_DAY;
|
||||||
|
|
||||||
|
export type FilterVariation = DeviceSecurityVariation.Verified
|
||||||
|
| DeviceSecurityVariation.Inactive
|
||||||
|
| DeviceSecurityVariation.Unverified;
|
||||||
|
|
||||||
export const isDeviceInactive: DeviceFilterCondition = device =>
|
export const isDeviceInactive: DeviceFilterCondition = device =>
|
||||||
!!device.last_seen_ts && device.last_seen_ts < Date.now() - INACTIVE_DEVICE_AGE_MS;
|
!!device.last_seen_ts && device.last_seen_ts < Date.now() - INACTIVE_DEVICE_AGE_MS;
|
||||||
|
|
||||||
const filters: Record<DeviceSecurityVariation, DeviceFilterCondition> = {
|
const filters: Record<FilterVariation, DeviceFilterCondition> = {
|
||||||
[DeviceSecurityVariation.Verified]: device => !!device.isVerified,
|
[DeviceSecurityVariation.Verified]: device => !!device.isVerified,
|
||||||
[DeviceSecurityVariation.Unverified]: device => !device.isVerified,
|
[DeviceSecurityVariation.Unverified]: device => !device.isVerified,
|
||||||
[DeviceSecurityVariation.Inactive]: isDeviceInactive,
|
[DeviceSecurityVariation.Inactive]: isDeviceInactive,
|
||||||
|
@ -33,7 +37,7 @@ const filters: Record<DeviceSecurityVariation, DeviceFilterCondition> = {
|
||||||
|
|
||||||
export const filterDevicesBySecurityRecommendation = (
|
export const filterDevicesBySecurityRecommendation = (
|
||||||
devices: ExtendedDevice[],
|
devices: ExtendedDevice[],
|
||||||
securityVariations: DeviceSecurityVariation[],
|
securityVariations: FilterVariation[],
|
||||||
) => {
|
) => {
|
||||||
const activeFilters = securityVariations.map(variation => filters[variation]);
|
const activeFilters = securityVariations.map(variation => filters[variation]);
|
||||||
if (!activeFilters.length) {
|
if (!activeFilters.length) {
|
||||||
|
|
|
@ -32,4 +32,7 @@ export enum DeviceSecurityVariation {
|
||||||
Verified = 'Verified',
|
Verified = 'Verified',
|
||||||
Unverified = 'Unverified',
|
Unverified = 'Unverified',
|
||||||
Inactive = 'Inactive',
|
Inactive = 'Inactive',
|
||||||
|
// sessions that do not support encryption
|
||||||
|
// eg a session that logged in via api to get an access token
|
||||||
|
Unverifiable = 'Unverifiable'
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@ import { useOwnDevices } from '../../devices/useOwnDevices';
|
||||||
import { FilteredDeviceList } from '../../devices/FilteredDeviceList';
|
import { FilteredDeviceList } from '../../devices/FilteredDeviceList';
|
||||||
import CurrentDeviceSection from '../../devices/CurrentDeviceSection';
|
import CurrentDeviceSection from '../../devices/CurrentDeviceSection';
|
||||||
import SecurityRecommendations from '../../devices/SecurityRecommendations';
|
import SecurityRecommendations from '../../devices/SecurityRecommendations';
|
||||||
import { DeviceSecurityVariation, ExtendedDevice } from '../../devices/types';
|
import { ExtendedDevice } from '../../devices/types';
|
||||||
import { deleteDevicesWithInteractiveAuth } from '../../devices/deleteDevices';
|
import { deleteDevicesWithInteractiveAuth } from '../../devices/deleteDevices';
|
||||||
import SettingsTab from '../SettingsTab';
|
import SettingsTab from '../SettingsTab';
|
||||||
import LoginWithQRSection from '../../devices/LoginWithQRSection';
|
import LoginWithQRSection from '../../devices/LoginWithQRSection';
|
||||||
|
@ -37,6 +37,7 @@ import LoginWithQR, { Mode } from '../../../auth/LoginWithQR';
|
||||||
import SettingsStore from '../../../../../settings/SettingsStore';
|
import SettingsStore from '../../../../../settings/SettingsStore';
|
||||||
import { useAsyncMemo } from '../../../../../hooks/useAsyncMemo';
|
import { useAsyncMemo } from '../../../../../hooks/useAsyncMemo';
|
||||||
import QuestionDialog from '../../../dialogs/QuestionDialog';
|
import QuestionDialog from '../../../dialogs/QuestionDialog';
|
||||||
|
import { FilterVariation } from '../../devices/filter';
|
||||||
|
|
||||||
const confirmSignOut = async (sessionsToSignOutCount: number): Promise<boolean> => {
|
const confirmSignOut = async (sessionsToSignOutCount: number): Promise<boolean> => {
|
||||||
const { finished } = Modal.createDialog(QuestionDialog, {
|
const { finished } = Modal.createDialog(QuestionDialog, {
|
||||||
|
@ -123,7 +124,7 @@ const SessionManagerTab: React.FC = () => {
|
||||||
setPushNotifications,
|
setPushNotifications,
|
||||||
supportsMSC3881,
|
supportsMSC3881,
|
||||||
} = useOwnDevices();
|
} = useOwnDevices();
|
||||||
const [filter, setFilter] = useState<DeviceSecurityVariation>();
|
const [filter, setFilter] = useState<FilterVariation>();
|
||||||
const [expandedDeviceIds, setExpandedDeviceIds] = useState<ExtendedDevice['device_id'][]>([]);
|
const [expandedDeviceIds, setExpandedDeviceIds] = useState<ExtendedDevice['device_id'][]>([]);
|
||||||
const [selectedDeviceIds, setSelectedDeviceIds] = useState<ExtendedDevice['device_id'][]>([]);
|
const [selectedDeviceIds, setSelectedDeviceIds] = useState<ExtendedDevice['device_id'][]>([]);
|
||||||
const filteredDeviceListRef = useRef<HTMLDivElement>(null);
|
const filteredDeviceListRef = useRef<HTMLDivElement>(null);
|
||||||
|
@ -142,7 +143,7 @@ const SessionManagerTab: React.FC = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onGoToFilteredList = (filter: DeviceSecurityVariation) => {
|
const onGoToFilteredList = (filter: FilterVariation) => {
|
||||||
setFilter(filter);
|
setFilter(filter);
|
||||||
clearTimeout(scrollIntoViewTimeoutRef.current);
|
clearTimeout(scrollIntoViewTimeoutRef.current);
|
||||||
// wait a tick for the filtered section to rerender with different height
|
// wait a tick for the filtered section to rerender with different height
|
||||||
|
|
|
@ -1801,6 +1801,10 @@
|
||||||
"Unverified sessions": "Unverified sessions",
|
"Unverified sessions": "Unverified sessions",
|
||||||
"Unverified sessions are sessions that have logged in with your credentials but have not been cross-verified.": "Unverified sessions are sessions that have logged in with your credentials but have not been cross-verified.",
|
"Unverified sessions are sessions that have logged in with your credentials but have not been cross-verified.": "Unverified sessions are sessions that have logged in with your credentials but have not been cross-verified.",
|
||||||
"You should make especially certain that you recognise these sessions as they could represent an unauthorised use of your account.": "You should make especially certain that you recognise these sessions as they could represent an unauthorised use of your account.",
|
"You should make especially certain that you recognise these sessions as they could represent an unauthorised use of your account.": "You should make especially certain that you recognise these sessions as they could represent an unauthorised use of your account.",
|
||||||
|
"Unverified session": "Unverified session",
|
||||||
|
"This session doesn't support encryption, so it can't be verified.": "This session doesn't support encryption, so it can't be verified.",
|
||||||
|
"You won't be able to participate in rooms where encryption is enabled when using this session.": "You won't be able to participate in rooms where encryption is enabled when using this session.",
|
||||||
|
"For best security and privacy, it is recommended to use Matrix clients that support encryption.": "For best security and privacy, it is recommended to use Matrix clients that support encryption.",
|
||||||
"Inactive sessions": "Inactive sessions",
|
"Inactive sessions": "Inactive sessions",
|
||||||
"Inactive sessions are sessions you have not used in some time, but they continue to receive encryption keys.": "Inactive sessions are sessions you have not used in some time, but they continue to receive encryption keys.",
|
"Inactive sessions are sessions you have not used in some time, but they continue to receive encryption keys.": "Inactive sessions are sessions you have not used in some time, but they continue to receive encryption keys.",
|
||||||
"Removing inactive sessions improves security and performance, and makes it easier for you to identify if a new session is suspicious.": "Removing inactive sessions improves security and performance, and makes it easier for you to identify if a new session is suspicious.",
|
"Removing inactive sessions improves security and performance, and makes it easier for you to identify if a new session is suspicious.": "Removing inactive sessions improves security and performance, and makes it easier for you to identify if a new session is suspicious.",
|
||||||
|
@ -1813,7 +1817,7 @@
|
||||||
"Unknown session type": "Unknown session type",
|
"Unknown session type": "Unknown session type",
|
||||||
"Verified session": "Verified session",
|
"Verified session": "Verified session",
|
||||||
"This session is ready for secure messaging.": "This session is ready for secure messaging.",
|
"This session is ready for secure messaging.": "This session is ready for secure messaging.",
|
||||||
"Unverified session": "Unverified session",
|
"This session doesn't support encryption and thus can't be verified.": "This session doesn't support encryption and thus can't be verified.",
|
||||||
"Verify or sign out from this session for best security and reliability.": "Verify or sign out from this session for best security and reliability.",
|
"Verify or sign out from this session for best security and reliability.": "Verify or sign out from this session for best security and reliability.",
|
||||||
"Verify session": "Verify session",
|
"Verify session": "Verify session",
|
||||||
"For best security, sign out from any session that you don't recognize or use anymore.": "For best security, sign out from any session that you don't recognize or use anymore.",
|
"For best security, sign out from any session that you don't recognize or use anymore.": "For best security, sign out from any session that you don't recognize or use anymore.",
|
||||||
|
|
|
@ -255,12 +255,23 @@ describe('<SessionManagerTab />', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets device verification status correctly', async () => {
|
it('sets device verification status correctly', async () => {
|
||||||
mockClient.getDevices.mockResolvedValue({ devices: [alicesDevice, alicesMobileDevice] });
|
mockClient.getDevices.mockResolvedValue({ devices:
|
||||||
|
[alicesDevice, alicesMobileDevice, alicesOlderMobileDevice],
|
||||||
|
});
|
||||||
|
mockClient.getStoredDevice.mockImplementation((_userId, deviceId) => new DeviceInfo(deviceId));
|
||||||
mockCrossSigningInfo.checkDeviceTrust
|
mockCrossSigningInfo.checkDeviceTrust
|
||||||
|
.mockImplementation((_userId, { deviceId }) => {
|
||||||
// alices device is trusted
|
// alices device is trusted
|
||||||
.mockReturnValueOnce(new DeviceTrustLevel(true, true, false, false))
|
if (deviceId === alicesDevice.device_id) {
|
||||||
|
return new DeviceTrustLevel(true, true, false, false);
|
||||||
|
}
|
||||||
// alices mobile device is not
|
// alices mobile device is not
|
||||||
.mockReturnValueOnce(new DeviceTrustLevel(false, false, false, false));
|
if (deviceId === alicesMobileDevice.device_id) {
|
||||||
|
return new DeviceTrustLevel(false, false, false, false);
|
||||||
|
}
|
||||||
|
// alicesOlderMobileDevice does not support encryption
|
||||||
|
throw new Error('encryption not supported');
|
||||||
|
});
|
||||||
|
|
||||||
const { getByTestId } = render(getComponent());
|
const { getByTestId } = render(getComponent());
|
||||||
|
|
||||||
|
@ -268,8 +279,20 @@ describe('<SessionManagerTab />', () => {
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockCrossSigningInfo.checkDeviceTrust).toHaveBeenCalledTimes(2);
|
expect(mockCrossSigningInfo.checkDeviceTrust).toHaveBeenCalledTimes(3);
|
||||||
expect(getByTestId(`device-tile-${alicesDevice.device_id}`)).toMatchSnapshot();
|
expect(
|
||||||
|
getByTestId(`device-tile-${alicesDevice.device_id}`)
|
||||||
|
.querySelector('[aria-label="Verified"]'),
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
getByTestId(`device-tile-${alicesMobileDevice.device_id}`)
|
||||||
|
.querySelector('[aria-label="Unverified"]'),
|
||||||
|
).toBeTruthy();
|
||||||
|
// sessions that dont support encryption use unverified badge
|
||||||
|
expect(
|
||||||
|
getByTestId(`device-tile-${alicesOlderMobileDevice.device_id}`)
|
||||||
|
.querySelector('[aria-label="Unverified"]'),
|
||||||
|
).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('extends device with client information when available', async () => {
|
it('extends device with client information when available', async () => {
|
||||||
|
@ -489,7 +512,7 @@ describe('<SessionManagerTab />', () => {
|
||||||
if (deviceId === alicesDevice.device_id) {
|
if (deviceId === alicesDevice.device_id) {
|
||||||
return new DeviceTrustLevel(true, true, false, false);
|
return new DeviceTrustLevel(true, true, false, false);
|
||||||
}
|
}
|
||||||
throw new Error('everything else unverified');
|
return new DeviceTrustLevel(false, false, false, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
const { getByTestId } = render(getComponent());
|
const { getByTestId } = render(getComponent());
|
||||||
|
@ -507,6 +530,38 @@ describe('<SessionManagerTab />', () => {
|
||||||
expect(modalSpy).toHaveBeenCalled();
|
expect(modalSpy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not allow device verification on session that do not support encryption', async () => {
|
||||||
|
mockClient.getDevices.mockResolvedValue({ devices: [alicesDevice, alicesMobileDevice] });
|
||||||
|
mockClient.getStoredDevice.mockImplementation((_userId, deviceId) => new DeviceInfo(deviceId));
|
||||||
|
mockCrossSigningInfo.checkDeviceTrust
|
||||||
|
.mockImplementation((_userId, { deviceId }) => {
|
||||||
|
// current session verified = able to verify other sessions
|
||||||
|
if (deviceId === alicesDevice.device_id) {
|
||||||
|
return new DeviceTrustLevel(true, true, false, false);
|
||||||
|
}
|
||||||
|
// but alicesMobileDevice doesn't support encryption
|
||||||
|
throw new Error('encryption not supported');
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
getByTestId,
|
||||||
|
queryByTestId,
|
||||||
|
} = render(getComponent());
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await flushPromises();
|
||||||
|
});
|
||||||
|
|
||||||
|
toggleDeviceDetails(getByTestId, alicesMobileDevice.device_id);
|
||||||
|
|
||||||
|
// no verify button
|
||||||
|
expect(queryByTestId(`verification-status-button-${alicesMobileDevice.device_id}`)).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
getByTestId(`device-detail-${alicesMobileDevice.device_id}`)
|
||||||
|
.getElementsByClassName('mx_DeviceSecurityCard'),
|
||||||
|
).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
it('refreshes devices after verifying other device', async () => {
|
it('refreshes devices after verifying other device', async () => {
|
||||||
const modalSpy = jest.spyOn(Modal, 'createDialog');
|
const modalSpy = jest.spyOn(Modal, 'createDialog');
|
||||||
|
|
||||||
|
@ -518,7 +573,7 @@ describe('<SessionManagerTab />', () => {
|
||||||
if (deviceId === alicesDevice.device_id) {
|
if (deviceId === alicesDevice.device_id) {
|
||||||
return new DeviceTrustLevel(true, true, false, false);
|
return new DeviceTrustLevel(true, true, false, false);
|
||||||
}
|
}
|
||||||
throw new Error('everything else unverified');
|
return new DeviceTrustLevel(false, false, false, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
const { getByTestId } = render(getComponent());
|
const { getByTestId } = render(getComponent());
|
||||||
|
|
|
@ -1,5 +1,43 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<SessionManagerTab /> Device verification does not allow device verification on session that do not support encryption 1`] = `
|
||||||
|
HTMLCollection [
|
||||||
|
<div
|
||||||
|
class="mx_DeviceSecurityCard"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_DeviceSecurityCard_icon Unverified"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
height="16"
|
||||||
|
width="16"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_DeviceSecurityCard_content"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
class="mx_DeviceSecurityCard_heading"
|
||||||
|
>
|
||||||
|
Unverified session
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="mx_DeviceSecurityCard_description"
|
||||||
|
>
|
||||||
|
This session doesn't support encryption and thus can't be verified.
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_LearnMore_button mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
Learn more
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`<SessionManagerTab /> Sign out Signs out of current device 1`] = `
|
exports[`<SessionManagerTab /> Sign out Signs out of current device 1`] = `
|
||||||
<div
|
<div
|
||||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger_inline"
|
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger_inline"
|
||||||
|
@ -345,68 +383,3 @@ exports[`<SessionManagerTab /> goes to filtered list from security recommendatio
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<SessionManagerTab /> sets device verification status correctly 1`] = `
|
|
||||||
<div
|
|
||||||
class="mx_DeviceTile mx_DeviceTile_interactive"
|
|
||||||
data-testid="device-tile-alices_device"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_DeviceTypeIcon"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_DeviceTypeIcon_deviceIconWrapper"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
aria-label="Unknown session type"
|
|
||||||
class="mx_DeviceTypeIcon_deviceIcon"
|
|
||||||
role="img"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
aria-label="Verified"
|
|
||||||
class="mx_DeviceTypeIcon_verificationIcon verified"
|
|
||||||
role="img"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="mx_DeviceTile_info"
|
|
||||||
>
|
|
||||||
<h4
|
|
||||||
class="mx_Heading_h4"
|
|
||||||
>
|
|
||||||
Alices device
|
|
||||||
</h4>
|
|
||||||
<div
|
|
||||||
class="mx_DeviceTile_metadata"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-testid="device-metadata-isVerified"
|
|
||||||
>
|
|
||||||
Verified
|
|
||||||
</span>
|
|
||||||
·
|
|
||||||
<span
|
|
||||||
data-testid="device-metadata-deviceId"
|
|
||||||
>
|
|
||||||
alices_device
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="mx_DeviceTile_actions"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
aria-label="Show details"
|
|
||||||
class="mx_AccessibleButton mx_DeviceExpandDetailsButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_icon"
|
|
||||||
data-testid="current-session-toggle-details"
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_DeviceExpandDetailsButton_icon"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue