OIDC: disable multi session signout for OIDC-aware servers in session manager (#11431)
* util for account url * test cases * disable multi session selection on device list * remove sign out all from context menus when oidc-aware * comment * remove unused param * typo
This commit is contained in:
parent
3c52ba0c92
commit
dfded8d4d3
9 changed files with 362 additions and 61 deletions
|
@ -31,6 +31,7 @@ import { DevicesState } from "./useOwnDevices";
|
|||
import FilteredDeviceListHeader from "./FilteredDeviceListHeader";
|
||||
import Spinner from "../../elements/Spinner";
|
||||
import { DeviceSecurityLearnMore } from "./DeviceSecurityLearnMore";
|
||||
import DeviceTile from "./DeviceTile";
|
||||
|
||||
interface Props {
|
||||
devices: DevicesDictionary;
|
||||
|
@ -48,6 +49,11 @@ interface Props {
|
|||
setPushNotifications: (deviceId: string, enabled: boolean) => Promise<void>;
|
||||
setSelectedDeviceIds: (deviceIds: ExtendedDevice["device_id"][]) => void;
|
||||
supportsMSC3881?: boolean | undefined;
|
||||
/**
|
||||
* Only allow sessions to be signed out individually
|
||||
* Removes checkboxes and multi selection header
|
||||
*/
|
||||
disableMultipleSignout?: boolean;
|
||||
}
|
||||
|
||||
const isDeviceSelected = (
|
||||
|
@ -178,6 +184,7 @@ const DeviceListItem: React.FC<{
|
|||
toggleSelected: () => void;
|
||||
setPushNotifications: (deviceId: string, enabled: boolean) => Promise<void>;
|
||||
supportsMSC3881?: boolean | undefined;
|
||||
isSelectDisabled?: boolean;
|
||||
}> = ({
|
||||
device,
|
||||
pusher,
|
||||
|
@ -192,33 +199,47 @@ const DeviceListItem: React.FC<{
|
|||
setPushNotifications,
|
||||
toggleSelected,
|
||||
supportsMSC3881,
|
||||
}) => (
|
||||
<li className="mx_FilteredDeviceList_listItem">
|
||||
<SelectableDeviceTile
|
||||
isSelected={isSelected}
|
||||
onSelect={toggleSelected}
|
||||
onClick={onDeviceExpandToggle}
|
||||
device={device}
|
||||
>
|
||||
isSelectDisabled,
|
||||
}) => {
|
||||
const tileContent = (
|
||||
<>
|
||||
{isSigningOut && <Spinner w={16} h={16} />}
|
||||
<DeviceExpandDetailsButton isExpanded={isExpanded} onClick={onDeviceExpandToggle} />
|
||||
</SelectableDeviceTile>
|
||||
{isExpanded && (
|
||||
<DeviceDetails
|
||||
device={device}
|
||||
pusher={pusher}
|
||||
localNotificationSettings={localNotificationSettings}
|
||||
isSigningOut={isSigningOut}
|
||||
onVerifyDevice={onRequestDeviceVerification}
|
||||
onSignOutDevice={onSignOutDevice}
|
||||
saveDeviceName={saveDeviceName}
|
||||
setPushNotifications={setPushNotifications}
|
||||
supportsMSC3881={supportsMSC3881}
|
||||
className="mx_FilteredDeviceList_deviceDetails"
|
||||
/>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<li className="mx_FilteredDeviceList_listItem">
|
||||
{isSelectDisabled ? (
|
||||
<DeviceTile device={device} onClick={onDeviceExpandToggle}>
|
||||
{tileContent}
|
||||
</DeviceTile>
|
||||
) : (
|
||||
<SelectableDeviceTile
|
||||
isSelected={isSelected}
|
||||
onSelect={toggleSelected}
|
||||
onClick={onDeviceExpandToggle}
|
||||
device={device}
|
||||
>
|
||||
{tileContent}
|
||||
</SelectableDeviceTile>
|
||||
)}
|
||||
{isExpanded && (
|
||||
<DeviceDetails
|
||||
device={device}
|
||||
pusher={pusher}
|
||||
localNotificationSettings={localNotificationSettings}
|
||||
isSigningOut={isSigningOut}
|
||||
onVerifyDevice={onRequestDeviceVerification}
|
||||
onSignOutDevice={onSignOutDevice}
|
||||
saveDeviceName={saveDeviceName}
|
||||
setPushNotifications={setPushNotifications}
|
||||
supportsMSC3881={supportsMSC3881}
|
||||
className="mx_FilteredDeviceList_deviceDetails"
|
||||
/>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Filtered list of devices
|
||||
|
@ -242,6 +263,7 @@ export const FilteredDeviceList = forwardRef(
|
|||
setPushNotifications,
|
||||
setSelectedDeviceIds,
|
||||
supportsMSC3881,
|
||||
disableMultipleSignout,
|
||||
}: Props,
|
||||
ref: ForwardedRef<HTMLDivElement>,
|
||||
) => {
|
||||
|
@ -302,6 +324,7 @@ export const FilteredDeviceList = forwardRef(
|
|||
selectedDeviceCount={selectedDeviceIds.length}
|
||||
isAllSelected={isAllSelected}
|
||||
toggleSelectAll={toggleSelectAll}
|
||||
isSelectDisabled={disableMultipleSignout}
|
||||
>
|
||||
{selectedDeviceIds.length ? (
|
||||
<>
|
||||
|
@ -351,6 +374,7 @@ export const FilteredDeviceList = forwardRef(
|
|||
isExpanded={expandedDeviceIds.includes(device.device_id)}
|
||||
isSigningOut={signingOutDeviceIds.includes(device.device_id)}
|
||||
isSelected={isDeviceSelected(device.device_id, selectedDeviceIds)}
|
||||
isSelectDisabled={disableMultipleSignout}
|
||||
onDeviceExpandToggle={() => onDeviceExpandToggle(device.device_id)}
|
||||
onSignOutDevice={() => onSignOutDevices([device.device_id])}
|
||||
saveDeviceName={(deviceName: string) => saveDeviceName(device.device_id, deviceName)}
|
||||
|
|
|
@ -24,6 +24,7 @@ import TooltipTarget from "../../elements/TooltipTarget";
|
|||
interface Props extends Omit<HTMLProps<HTMLDivElement>, "className"> {
|
||||
selectedDeviceCount: number;
|
||||
isAllSelected: boolean;
|
||||
isSelectDisabled?: boolean;
|
||||
toggleSelectAll: () => void;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
@ -31,6 +32,7 @@ interface Props extends Omit<HTMLProps<HTMLDivElement>, "className"> {
|
|||
const FilteredDeviceListHeader: React.FC<Props> = ({
|
||||
selectedDeviceCount,
|
||||
isAllSelected,
|
||||
isSelectDisabled,
|
||||
toggleSelectAll,
|
||||
children,
|
||||
...rest
|
||||
|
@ -38,16 +40,18 @@ const FilteredDeviceListHeader: React.FC<Props> = ({
|
|||
const checkboxLabel = isAllSelected ? _t("Deselect all") : _t("Select all");
|
||||
return (
|
||||
<div className="mx_FilteredDeviceListHeader" {...rest}>
|
||||
<TooltipTarget label={checkboxLabel} alignment={Alignment.Top}>
|
||||
<StyledCheckbox
|
||||
kind={CheckboxStyle.Solid}
|
||||
checked={isAllSelected}
|
||||
onChange={toggleSelectAll}
|
||||
id="device-select-all-checkbox"
|
||||
data-testid="device-select-all-checkbox"
|
||||
aria-label={checkboxLabel}
|
||||
/>
|
||||
</TooltipTarget>
|
||||
{!isSelectDisabled && (
|
||||
<TooltipTarget label={checkboxLabel} alignment={Alignment.Top}>
|
||||
<StyledCheckbox
|
||||
kind={CheckboxStyle.Solid}
|
||||
checked={isAllSelected}
|
||||
onChange={toggleSelectAll}
|
||||
id="device-select-all-checkbox"
|
||||
data-testid="device-select-all-checkbox"
|
||||
aria-label={checkboxLabel}
|
||||
/>
|
||||
</TooltipTarget>
|
||||
)}
|
||||
<span className="mx_FilteredDeviceListHeader_label">
|
||||
{selectedDeviceCount > 0
|
||||
? _t("%(count)s sessions selected", { count: selectedDeviceCount })
|
||||
|
|
|
@ -20,6 +20,7 @@ import { _t } from "../../../../languageHandler";
|
|||
import { KebabContextMenu } from "../../context_menus/KebabContextMenu";
|
||||
import { SettingsSubsectionHeading } from "../shared/SettingsSubsectionHeading";
|
||||
import { IconizedContextMenuOption } from "../../context_menus/IconizedContextMenu";
|
||||
import { filterBoolean } from "../../../../utils/arrays";
|
||||
|
||||
interface Props {
|
||||
// total count of other sessions
|
||||
|
@ -27,7 +28,8 @@ interface Props {
|
|||
// not affected by filters
|
||||
otherSessionsCount: number;
|
||||
disabled?: boolean;
|
||||
signOutAllOtherSessions: () => void;
|
||||
// not provided when sign out all other sessions is not available
|
||||
signOutAllOtherSessions?: () => void;
|
||||
}
|
||||
|
||||
export const OtherSessionsSectionHeading: React.FC<Props> = ({
|
||||
|
@ -35,22 +37,26 @@ export const OtherSessionsSectionHeading: React.FC<Props> = ({
|
|||
disabled,
|
||||
signOutAllOtherSessions,
|
||||
}) => {
|
||||
const menuOptions = [
|
||||
<IconizedContextMenuOption
|
||||
key="sign-out-all-others"
|
||||
label={_t("Sign out of %(count)s sessions", { count: otherSessionsCount })}
|
||||
onClick={signOutAllOtherSessions}
|
||||
isDestructive
|
||||
/>,
|
||||
];
|
||||
const menuOptions = filterBoolean([
|
||||
signOutAllOtherSessions ? (
|
||||
<IconizedContextMenuOption
|
||||
key="sign-out-all-others"
|
||||
label={_t("Sign out of %(count)s sessions", { count: otherSessionsCount })}
|
||||
onClick={signOutAllOtherSessions}
|
||||
isDestructive
|
||||
/>
|
||||
) : null,
|
||||
]);
|
||||
return (
|
||||
<SettingsSubsectionHeading heading={_t("Other sessions")}>
|
||||
<KebabContextMenu
|
||||
disabled={disabled}
|
||||
title={_t("Options")}
|
||||
options={menuOptions}
|
||||
data-testid="other-sessions-menu"
|
||||
/>
|
||||
{!!menuOptions.length && (
|
||||
<KebabContextMenu
|
||||
disabled={disabled}
|
||||
title={_t("Options")}
|
||||
options={menuOptions}
|
||||
data-testid="other-sessions-menu"
|
||||
/>
|
||||
)}
|
||||
</SettingsSubsectionHeading>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, { ReactNode } from "react";
|
||||
import { SERVICE_TYPES, IDelegatedAuthConfig, M_AUTHENTICATION, HTTPError } from "matrix-js-sdk/src/matrix";
|
||||
import { SERVICE_TYPES, HTTPError } from "matrix-js-sdk/src/matrix";
|
||||
import { IThreepid, ThreepidMedium } from "matrix-js-sdk/src/@types/threepids";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
|
@ -59,6 +59,7 @@ import Heading from "../../../typography/Heading";
|
|||
import InlineSpinner from "../../../elements/InlineSpinner";
|
||||
import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
|
||||
import { ThirdPartyIdentifier } from "../../../../../AddThreepid";
|
||||
import { getDelegatedAuthAccountUrl } from "../../../../../utils/oidc/getDelegatedAuthAccountUrl";
|
||||
|
||||
interface IProps {
|
||||
closeSettingsFn: () => void;
|
||||
|
@ -172,8 +173,7 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
|
|||
// the enabled flag value.
|
||||
const canChangePassword = !changePasswordCap || changePasswordCap["enabled"] !== false;
|
||||
|
||||
const delegatedAuthConfig = M_AUTHENTICATION.findIn<IDelegatedAuthConfig | undefined>(cli.getClientWellKnown());
|
||||
const externalAccountManagementUrl = delegatedAuthConfig?.account;
|
||||
const externalAccountManagementUrl = getDelegatedAuthAccountUrl(cli.getClientWellKnown());
|
||||
|
||||
this.setState({ canChangePassword, externalAccountManagementUrl });
|
||||
}
|
||||
|
|
|
@ -39,6 +39,7 @@ import QuestionDialog from "../../../dialogs/QuestionDialog";
|
|||
import { FilterVariation } from "../../devices/filter";
|
||||
import { OtherSessionsSectionHeading } from "../../devices/OtherSessionsSectionHeading";
|
||||
import { SettingsSection } from "../../shared/SettingsSection";
|
||||
import { getDelegatedAuthAccountUrl } from "../../../../../utils/oidc/getDelegatedAuthAccountUrl";
|
||||
|
||||
const confirmSignOut = async (sessionsToSignOutCount: number): Promise<boolean> => {
|
||||
const { finished } = Modal.createDialog(QuestionDialog, {
|
||||
|
@ -130,6 +131,14 @@ const SessionManagerTab: React.FC = () => {
|
|||
const scrollIntoViewTimeoutRef = useRef<number>();
|
||||
|
||||
const matrixClient = useContext(MatrixClientContext);
|
||||
/**
|
||||
* If we have a delegated auth account management URL, all sessions but the current session need to be managed in the
|
||||
* delegated auth provider.
|
||||
* See https://github.com/matrix-org/matrix-spec-proposals/pull/3824
|
||||
*/
|
||||
const delegatedAuthAccountUrl = getDelegatedAuthAccountUrl(matrixClient.getClientWellKnown());
|
||||
const disableMultipleSignout = !!delegatedAuthAccountUrl;
|
||||
|
||||
const userId = matrixClient?.getUserId();
|
||||
const currentUserMember = (userId && matrixClient?.getUser(userId)) || undefined;
|
||||
const clientVersions = useAsyncMemo(() => matrixClient.getVersions(), [matrixClient]);
|
||||
|
@ -205,11 +214,12 @@ const SessionManagerTab: React.FC = () => {
|
|||
setSelectedDeviceIds([]);
|
||||
}, [filter, setSelectedDeviceIds]);
|
||||
|
||||
const signOutAllOtherSessions = shouldShowOtherSessions
|
||||
? () => {
|
||||
onSignOutOtherDevices(Object.keys(otherDevices));
|
||||
}
|
||||
: undefined;
|
||||
const signOutAllOtherSessions =
|
||||
shouldShowOtherSessions && !disableMultipleSignout
|
||||
? () => {
|
||||
onSignOutOtherDevices(Object.keys(otherDevices));
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const [signInWithQrMode, setSignInWithQrMode] = useState<Mode | null>();
|
||||
|
||||
|
@ -250,7 +260,7 @@ const SessionManagerTab: React.FC = () => {
|
|||
heading={
|
||||
<OtherSessionsSectionHeading
|
||||
otherSessionsCount={otherSessionsCount}
|
||||
signOutAllOtherSessions={signOutAllOtherSessions!}
|
||||
signOutAllOtherSessions={signOutAllOtherSessions}
|
||||
disabled={!!signingOutDeviceIds.length}
|
||||
/>
|
||||
}
|
||||
|
@ -280,6 +290,7 @@ const SessionManagerTab: React.FC = () => {
|
|||
setPushNotifications={setPushNotifications}
|
||||
ref={filteredDeviceListRef}
|
||||
supportsMSC3881={supportsMSC3881}
|
||||
disableMultipleSignout={disableMultipleSignout}
|
||||
/>
|
||||
</SettingsSubsection>
|
||||
)}
|
||||
|
|
27
src/utils/oidc/getDelegatedAuthAccountUrl.ts
Normal file
27
src/utils/oidc/getDelegatedAuthAccountUrl.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { IClientWellKnown, IDelegatedAuthConfig, M_AUTHENTICATION } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
/**
|
||||
* Get the delegated auth account management url if configured
|
||||
* @param clientWellKnown from MatrixClient.getClientWellKnown
|
||||
* @returns the account management url, or undefined
|
||||
*/
|
||||
export const getDelegatedAuthAccountUrl = (clientWellKnown: IClientWellKnown | undefined): string | undefined => {
|
||||
const delegatedAuthConfig = M_AUTHENTICATION.findIn<IDelegatedAuthConfig | undefined>(clientWellKnown);
|
||||
return delegatedAuthConfig?.account;
|
||||
};
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import { act, fireEvent, render, RenderResult } from "@testing-library/react";
|
||||
import { act, fireEvent, render, RenderResult, screen } from "@testing-library/react";
|
||||
import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { VerificationRequest } from "matrix-js-sdk/src/crypto-api";
|
||||
|
@ -32,6 +32,7 @@ import {
|
|||
CryptoApi,
|
||||
DeviceVerificationStatus,
|
||||
MatrixError,
|
||||
M_AUTHENTICATION,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
|
@ -87,7 +88,7 @@ describe("<SessionManagerTab />", () => {
|
|||
requestDeviceVerification: jest.fn().mockResolvedValue(mockVerificationRequest),
|
||||
} as unknown as CryptoApi);
|
||||
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
let mockClient = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(aliceId),
|
||||
getCrypto: jest.fn().mockReturnValue(mockCrypto),
|
||||
getDevices: jest.fn(),
|
||||
|
@ -104,6 +105,7 @@ describe("<SessionManagerTab />", () => {
|
|||
setLocalNotificationSettings: jest.fn(),
|
||||
getVersions: jest.fn().mockResolvedValue({}),
|
||||
getCapabilities: jest.fn().mockResolvedValue({}),
|
||||
getClientWellKnown: jest.fn().mockReturnValue({}),
|
||||
});
|
||||
|
||||
const defaultProps = {};
|
||||
|
@ -173,6 +175,25 @@ describe("<SessionManagerTab />", () => {
|
|||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
mockClient = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(aliceId),
|
||||
getCrypto: jest.fn().mockReturnValue(mockCrypto),
|
||||
getDevices: jest.fn(),
|
||||
getStoredDevice: jest.fn(),
|
||||
getDeviceId: jest.fn().mockReturnValue(deviceId),
|
||||
deleteMultipleDevices: jest.fn(),
|
||||
generateClientSecret: jest.fn(),
|
||||
setDeviceDetails: jest.fn(),
|
||||
getAccountData: jest.fn(),
|
||||
deleteAccountData: jest.fn(),
|
||||
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(true),
|
||||
getPushers: jest.fn(),
|
||||
setPusher: jest.fn(),
|
||||
setLocalNotificationSettings: jest.fn(),
|
||||
getVersions: jest.fn().mockResolvedValue({}),
|
||||
getCapabilities: jest.fn().mockResolvedValue({}),
|
||||
getClientWellKnown: jest.fn().mockReturnValue({}),
|
||||
});
|
||||
jest.clearAllMocks();
|
||||
jest.spyOn(logger, "error").mockRestore();
|
||||
mockClient.getStoredDevice.mockImplementation((_userId, id) => {
|
||||
|
@ -1012,6 +1033,138 @@ describe("<SessionManagerTab />", () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("for an OIDC-aware server", () => {
|
||||
beforeEach(() => {
|
||||
mockClient.getClientWellKnown.mockReturnValue({
|
||||
[M_AUTHENTICATION.name]: {
|
||||
issuer: "https://issuer.org",
|
||||
account: "https://issuer.org/account",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// signing out the current device works as usual
|
||||
it("Signs out of current device", async () => {
|
||||
const modalSpy = jest.spyOn(Modal, "createDialog");
|
||||
|
||||
mockClient.getDevices.mockResolvedValue({
|
||||
devices: [alicesDevice],
|
||||
});
|
||||
const { getByTestId } = render(getComponent());
|
||||
|
||||
await act(async () => {
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
toggleDeviceDetails(getByTestId, alicesDevice.device_id);
|
||||
|
||||
const signOutButton = getByTestId("device-detail-sign-out-cta");
|
||||
expect(signOutButton).toMatchSnapshot();
|
||||
fireEvent.click(signOutButton);
|
||||
|
||||
// logout dialog opened
|
||||
expect(modalSpy).toHaveBeenCalledWith(LogoutDialog, {}, undefined, false, true);
|
||||
});
|
||||
|
||||
it("does not allow signing out of all other devices from current session context menu", async () => {
|
||||
mockClient.getDevices.mockResolvedValue({
|
||||
devices: [alicesDevice, alicesMobileDevice, alicesOlderMobileDevice],
|
||||
});
|
||||
const { getByTestId } = render(getComponent());
|
||||
|
||||
await act(async () => {
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
fireEvent.click(getByTestId("current-session-menu"));
|
||||
expect(screen.queryByLabelText("Sign out of all other sessions (2)")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("other devices", () => {
|
||||
// signing out a single device still works
|
||||
// this test will be updated once redirect to MAS is added
|
||||
// https://github.com/vector-im/element-web/issues/26000
|
||||
it("deletes a device when interactive auth is not required", async () => {
|
||||
mockClient.deleteMultipleDevices.mockResolvedValue({});
|
||||
mockClient.getDevices
|
||||
.mockResolvedValueOnce({
|
||||
devices: [alicesDevice, alicesMobileDevice, alicesOlderMobileDevice],
|
||||
})
|
||||
// pretend it was really deleted on refresh
|
||||
.mockResolvedValueOnce({
|
||||
devices: [alicesDevice, alicesOlderMobileDevice],
|
||||
});
|
||||
|
||||
const { getByTestId } = render(getComponent());
|
||||
|
||||
await act(async () => {
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
toggleDeviceDetails(getByTestId, alicesMobileDevice.device_id);
|
||||
|
||||
const deviceDetails = getByTestId(`device-detail-${alicesMobileDevice.device_id}`);
|
||||
const signOutButton = deviceDetails.querySelector(
|
||||
'[data-testid="device-detail-sign-out-cta"]',
|
||||
) as Element;
|
||||
fireEvent.click(signOutButton);
|
||||
|
||||
await confirmSignout(getByTestId);
|
||||
|
||||
// sign out button is disabled with spinner
|
||||
expect(
|
||||
(
|
||||
deviceDetails.querySelector('[data-testid="device-detail-sign-out-cta"]') as Element
|
||||
).getAttribute("aria-disabled"),
|
||||
).toEqual("true");
|
||||
// delete called
|
||||
expect(mockClient.deleteMultipleDevices).toHaveBeenCalledWith(
|
||||
[alicesMobileDevice.device_id],
|
||||
undefined,
|
||||
);
|
||||
|
||||
await flushPromises();
|
||||
|
||||
// devices refreshed
|
||||
expect(mockClient.getDevices).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not allow removing multiple devices at once", async () => {
|
||||
mockClient.getDevices.mockResolvedValue({
|
||||
devices: [alicesDevice, alicesMobileDevice, alicesOlderMobileDevice, alicesInactiveDevice],
|
||||
});
|
||||
|
||||
render(getComponent());
|
||||
|
||||
await act(async () => {
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
// sessions don't have checkboxes
|
||||
expect(
|
||||
screen.queryByTestId(`device-tile-checkbox-${alicesMobileDevice.device_id}`),
|
||||
).not.toBeInTheDocument();
|
||||
// no select all
|
||||
expect(screen.queryByLabelText("Select all")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not allow signing out of all other devices from other sessions context menu", async () => {
|
||||
mockClient.getDevices.mockResolvedValue({
|
||||
devices: [alicesDevice, alicesMobileDevice, alicesOlderMobileDevice],
|
||||
});
|
||||
render(getComponent());
|
||||
|
||||
await act(async () => {
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
// no context menu because 'sign out all' is the only option
|
||||
// and it is not allowed when server is oidc-aware
|
||||
expect(screen.queryByTestId("other-sessions-menu")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Rename sessions", () => {
|
||||
|
|
|
@ -53,6 +53,21 @@ exports[`<SessionManagerTab /> Sign out Signs out of current device 1`] = `
|
|||
</div>
|
||||
`;
|
||||
|
||||
exports[`<SessionManagerTab /> Sign out for an OIDC-aware server Signs out of current device 1`] = `
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_danger_inline"
|
||||
data-testid="device-detail-sign-out-cta"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="mx_DeviceDetails_signOutButtonContent"
|
||||
>
|
||||
Sign out of this session
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<SessionManagerTab /> current session section renders current session section with a verified session 1`] = `
|
||||
<div
|
||||
class="mx_SettingsSubsection"
|
||||
|
|
61
test/utils/oidc/getDelegatedAuthAccountUrl-test.ts
Normal file
61
test/utils/oidc/getDelegatedAuthAccountUrl-test.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { M_AUTHENTICATION } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { getDelegatedAuthAccountUrl } from "../../../src/utils/oidc/getDelegatedAuthAccountUrl";
|
||||
|
||||
describe("getDelegatedAuthAccountUrl()", () => {
|
||||
it("should return undefined when wk is undefined", () => {
|
||||
expect(getDelegatedAuthAccountUrl(undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return undefined when wk has no authentication config", () => {
|
||||
expect(getDelegatedAuthAccountUrl({})).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return undefined when wk authentication config has no configured account url", () => {
|
||||
expect(
|
||||
getDelegatedAuthAccountUrl({
|
||||
[M_AUTHENTICATION.stable!]: {
|
||||
issuer: "issuer.org",
|
||||
},
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return the account url for authentication config using the unstable prefix", () => {
|
||||
expect(
|
||||
getDelegatedAuthAccountUrl({
|
||||
[M_AUTHENTICATION.unstable!]: {
|
||||
issuer: "issuer.org",
|
||||
account: "issuer.org/account",
|
||||
},
|
||||
}),
|
||||
).toEqual("issuer.org/account");
|
||||
});
|
||||
|
||||
it("should return the account url for authentication config using the stable prefix", () => {
|
||||
expect(
|
||||
getDelegatedAuthAccountUrl({
|
||||
[M_AUTHENTICATION.stable!]: {
|
||||
issuer: "issuer.org",
|
||||
account: "issuer.org/account",
|
||||
},
|
||||
}),
|
||||
).toEqual("issuer.org/account");
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue