Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit dfded8d

Browse files
author
Kerry
authored
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
1 parent 3c52ba0 commit dfded8d

File tree

9 files changed

+362
-61
lines changed

9 files changed

+362
-61
lines changed

src/components/views/settings/devices/FilteredDeviceList.tsx

Lines changed: 49 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { DevicesState } from "./useOwnDevices";
3131
import FilteredDeviceListHeader from "./FilteredDeviceListHeader";
3232
import Spinner from "../../elements/Spinner";
3333
import { DeviceSecurityLearnMore } from "./DeviceSecurityLearnMore";
34+
import DeviceTile from "./DeviceTile";
3435

3536
interface Props {
3637
devices: DevicesDictionary;
@@ -48,6 +49,11 @@ interface Props {
4849
setPushNotifications: (deviceId: string, enabled: boolean) => Promise<void>;
4950
setSelectedDeviceIds: (deviceIds: ExtendedDevice["device_id"][]) => void;
5051
supportsMSC3881?: boolean | undefined;
52+
/**
53+
* Only allow sessions to be signed out individually
54+
* Removes checkboxes and multi selection header
55+
*/
56+
disableMultipleSignout?: boolean;
5157
}
5258

5359
const isDeviceSelected = (
@@ -178,6 +184,7 @@ const DeviceListItem: React.FC<{
178184
toggleSelected: () => void;
179185
setPushNotifications: (deviceId: string, enabled: boolean) => Promise<void>;
180186
supportsMSC3881?: boolean | undefined;
187+
isSelectDisabled?: boolean;
181188
}> = ({
182189
device,
183190
pusher,
@@ -192,33 +199,47 @@ const DeviceListItem: React.FC<{
192199
setPushNotifications,
193200
toggleSelected,
194201
supportsMSC3881,
195-
}) => (
196-
<li className="mx_FilteredDeviceList_listItem">
197-
<SelectableDeviceTile
198-
isSelected={isSelected}
199-
onSelect={toggleSelected}
200-
onClick={onDeviceExpandToggle}
201-
device={device}
202-
>
202+
isSelectDisabled,
203+
}) => {
204+
const tileContent = (
205+
<>
203206
{isSigningOut && <Spinner w={16} h={16} />}
204207
<DeviceExpandDetailsButton isExpanded={isExpanded} onClick={onDeviceExpandToggle} />
205-
</SelectableDeviceTile>
206-
{isExpanded && (
207-
<DeviceDetails
208-
device={device}
209-
pusher={pusher}
210-
localNotificationSettings={localNotificationSettings}
211-
isSigningOut={isSigningOut}
212-
onVerifyDevice={onRequestDeviceVerification}
213-
onSignOutDevice={onSignOutDevice}
214-
saveDeviceName={saveDeviceName}
215-
setPushNotifications={setPushNotifications}
216-
supportsMSC3881={supportsMSC3881}
217-
className="mx_FilteredDeviceList_deviceDetails"
218-
/>
219-
)}
220-
</li>
221-
);
208+
</>
209+
);
210+
return (
211+
<li className="mx_FilteredDeviceList_listItem">
212+
{isSelectDisabled ? (
213+
<DeviceTile device={device} onClick={onDeviceExpandToggle}>
214+
{tileContent}
215+
</DeviceTile>
216+
) : (
217+
<SelectableDeviceTile
218+
isSelected={isSelected}
219+
onSelect={toggleSelected}
220+
onClick={onDeviceExpandToggle}
221+
device={device}
222+
>
223+
{tileContent}
224+
</SelectableDeviceTile>
225+
)}
226+
{isExpanded && (
227+
<DeviceDetails
228+
device={device}
229+
pusher={pusher}
230+
localNotificationSettings={localNotificationSettings}
231+
isSigningOut={isSigningOut}
232+
onVerifyDevice={onRequestDeviceVerification}
233+
onSignOutDevice={onSignOutDevice}
234+
saveDeviceName={saveDeviceName}
235+
setPushNotifications={setPushNotifications}
236+
supportsMSC3881={supportsMSC3881}
237+
className="mx_FilteredDeviceList_deviceDetails"
238+
/>
239+
)}
240+
</li>
241+
);
242+
};
222243

223244
/**
224245
* Filtered list of devices
@@ -242,6 +263,7 @@ export const FilteredDeviceList = forwardRef(
242263
setPushNotifications,
243264
setSelectedDeviceIds,
244265
supportsMSC3881,
266+
disableMultipleSignout,
245267
}: Props,
246268
ref: ForwardedRef<HTMLDivElement>,
247269
) => {
@@ -302,6 +324,7 @@ export const FilteredDeviceList = forwardRef(
302324
selectedDeviceCount={selectedDeviceIds.length}
303325
isAllSelected={isAllSelected}
304326
toggleSelectAll={toggleSelectAll}
327+
isSelectDisabled={disableMultipleSignout}
305328
>
306329
{selectedDeviceIds.length ? (
307330
<>
@@ -351,6 +374,7 @@ export const FilteredDeviceList = forwardRef(
351374
isExpanded={expandedDeviceIds.includes(device.device_id)}
352375
isSigningOut={signingOutDeviceIds.includes(device.device_id)}
353376
isSelected={isDeviceSelected(device.device_id, selectedDeviceIds)}
377+
isSelectDisabled={disableMultipleSignout}
354378
onDeviceExpandToggle={() => onDeviceExpandToggle(device.device_id)}
355379
onSignOutDevice={() => onSignOutDevices([device.device_id])}
356380
saveDeviceName={(deviceName: string) => saveDeviceName(device.device_id, deviceName)}

src/components/views/settings/devices/FilteredDeviceListHeader.tsx

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,30 +24,34 @@ import TooltipTarget from "../../elements/TooltipTarget";
2424
interface Props extends Omit<HTMLProps<HTMLDivElement>, "className"> {
2525
selectedDeviceCount: number;
2626
isAllSelected: boolean;
27+
isSelectDisabled?: boolean;
2728
toggleSelectAll: () => void;
2829
children?: React.ReactNode;
2930
}
3031

3132
const FilteredDeviceListHeader: React.FC<Props> = ({
3233
selectedDeviceCount,
3334
isAllSelected,
35+
isSelectDisabled,
3436
toggleSelectAll,
3537
children,
3638
...rest
3739
}) => {
3840
const checkboxLabel = isAllSelected ? _t("Deselect all") : _t("Select all");
3941
return (
4042
<div className="mx_FilteredDeviceListHeader" {...rest}>
41-
<TooltipTarget label={checkboxLabel} alignment={Alignment.Top}>
42-
<StyledCheckbox
43-
kind={CheckboxStyle.Solid}
44-
checked={isAllSelected}
45-
onChange={toggleSelectAll}
46-
id="device-select-all-checkbox"
47-
data-testid="device-select-all-checkbox"
48-
aria-label={checkboxLabel}
49-
/>
50-
</TooltipTarget>
43+
{!isSelectDisabled && (
44+
<TooltipTarget label={checkboxLabel} alignment={Alignment.Top}>
45+
<StyledCheckbox
46+
kind={CheckboxStyle.Solid}
47+
checked={isAllSelected}
48+
onChange={toggleSelectAll}
49+
id="device-select-all-checkbox"
50+
data-testid="device-select-all-checkbox"
51+
aria-label={checkboxLabel}
52+
/>
53+
</TooltipTarget>
54+
)}
5155
<span className="mx_FilteredDeviceListHeader_label">
5256
{selectedDeviceCount > 0
5357
? _t("%(count)s sessions selected", { count: selectedDeviceCount })

src/components/views/settings/devices/OtherSessionsSectionHeading.tsx

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,37 +20,43 @@ import { _t } from "../../../../languageHandler";
2020
import { KebabContextMenu } from "../../context_menus/KebabContextMenu";
2121
import { SettingsSubsectionHeading } from "../shared/SettingsSubsectionHeading";
2222
import { IconizedContextMenuOption } from "../../context_menus/IconizedContextMenu";
23+
import { filterBoolean } from "../../../../utils/arrays";
2324

2425
interface Props {
2526
// total count of other sessions
2627
// excludes current sessions
2728
// not affected by filters
2829
otherSessionsCount: number;
2930
disabled?: boolean;
30-
signOutAllOtherSessions: () => void;
31+
// not provided when sign out all other sessions is not available
32+
signOutAllOtherSessions?: () => void;
3133
}
3234

3335
export const OtherSessionsSectionHeading: React.FC<Props> = ({
3436
otherSessionsCount,
3537
disabled,
3638
signOutAllOtherSessions,
3739
}) => {
38-
const menuOptions = [
39-
<IconizedContextMenuOption
40-
key="sign-out-all-others"
41-
label={_t("Sign out of %(count)s sessions", { count: otherSessionsCount })}
42-
onClick={signOutAllOtherSessions}
43-
isDestructive
44-
/>,
45-
];
40+
const menuOptions = filterBoolean([
41+
signOutAllOtherSessions ? (
42+
<IconizedContextMenuOption
43+
key="sign-out-all-others"
44+
label={_t("Sign out of %(count)s sessions", { count: otherSessionsCount })}
45+
onClick={signOutAllOtherSessions}
46+
isDestructive
47+
/>
48+
) : null,
49+
]);
4650
return (
4751
<SettingsSubsectionHeading heading={_t("Other sessions")}>
48-
<KebabContextMenu
49-
disabled={disabled}
50-
title={_t("Options")}
51-
options={menuOptions}
52-
data-testid="other-sessions-menu"
53-
/>
52+
{!!menuOptions.length && (
53+
<KebabContextMenu
54+
disabled={disabled}
55+
title={_t("Options")}
56+
options={menuOptions}
57+
data-testid="other-sessions-menu"
58+
/>
59+
)}
5460
</SettingsSubsectionHeading>
5561
);
5662
};

src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ limitations under the License.
1717
*/
1818

1919
import React, { ReactNode } from "react";
20-
import { SERVICE_TYPES, IDelegatedAuthConfig, M_AUTHENTICATION, HTTPError } from "matrix-js-sdk/src/matrix";
20+
import { SERVICE_TYPES, HTTPError } from "matrix-js-sdk/src/matrix";
2121
import { IThreepid, ThreepidMedium } from "matrix-js-sdk/src/@types/threepids";
2222
import { logger } from "matrix-js-sdk/src/logger";
2323

@@ -59,6 +59,7 @@ import Heading from "../../../typography/Heading";
5959
import InlineSpinner from "../../../elements/InlineSpinner";
6060
import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
6161
import { ThirdPartyIdentifier } from "../../../../../AddThreepid";
62+
import { getDelegatedAuthAccountUrl } from "../../../../../utils/oidc/getDelegatedAuthAccountUrl";
6263

6364
interface IProps {
6465
closeSettingsFn: () => void;
@@ -172,8 +173,7 @@ export default class GeneralUserSettingsTab extends React.Component<IProps, ISta
172173
// the enabled flag value.
173174
const canChangePassword = !changePasswordCap || changePasswordCap["enabled"] !== false;
174175

175-
const delegatedAuthConfig = M_AUTHENTICATION.findIn<IDelegatedAuthConfig | undefined>(cli.getClientWellKnown());
176-
const externalAccountManagementUrl = delegatedAuthConfig?.account;
176+
const externalAccountManagementUrl = getDelegatedAuthAccountUrl(cli.getClientWellKnown());
177177

178178
this.setState({ canChangePassword, externalAccountManagementUrl });
179179
}

src/components/views/settings/tabs/user/SessionManagerTab.tsx

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import QuestionDialog from "../../../dialogs/QuestionDialog";
3939
import { FilterVariation } from "../../devices/filter";
4040
import { OtherSessionsSectionHeading } from "../../devices/OtherSessionsSectionHeading";
4141
import { SettingsSection } from "../../shared/SettingsSection";
42+
import { getDelegatedAuthAccountUrl } from "../../../../../utils/oidc/getDelegatedAuthAccountUrl";
4243

4344
const confirmSignOut = async (sessionsToSignOutCount: number): Promise<boolean> => {
4445
const { finished } = Modal.createDialog(QuestionDialog, {
@@ -130,6 +131,14 @@ const SessionManagerTab: React.FC = () => {
130131
const scrollIntoViewTimeoutRef = useRef<number>();
131132

132133
const matrixClient = useContext(MatrixClientContext);
134+
/**
135+
* If we have a delegated auth account management URL, all sessions but the current session need to be managed in the
136+
* delegated auth provider.
137+
* See https://github.com/matrix-org/matrix-spec-proposals/pull/3824
138+
*/
139+
const delegatedAuthAccountUrl = getDelegatedAuthAccountUrl(matrixClient.getClientWellKnown());
140+
const disableMultipleSignout = !!delegatedAuthAccountUrl;
141+
133142
const userId = matrixClient?.getUserId();
134143
const currentUserMember = (userId && matrixClient?.getUser(userId)) || undefined;
135144
const clientVersions = useAsyncMemo(() => matrixClient.getVersions(), [matrixClient]);
@@ -205,11 +214,12 @@ const SessionManagerTab: React.FC = () => {
205214
setSelectedDeviceIds([]);
206215
}, [filter, setSelectedDeviceIds]);
207216

208-
const signOutAllOtherSessions = shouldShowOtherSessions
209-
? () => {
210-
onSignOutOtherDevices(Object.keys(otherDevices));
211-
}
212-
: undefined;
217+
const signOutAllOtherSessions =
218+
shouldShowOtherSessions && !disableMultipleSignout
219+
? () => {
220+
onSignOutOtherDevices(Object.keys(otherDevices));
221+
}
222+
: undefined;
213223

214224
const [signInWithQrMode, setSignInWithQrMode] = useState<Mode | null>();
215225

@@ -250,7 +260,7 @@ const SessionManagerTab: React.FC = () => {
250260
heading={
251261
<OtherSessionsSectionHeading
252262
otherSessionsCount={otherSessionsCount}
253-
signOutAllOtherSessions={signOutAllOtherSessions!}
263+
signOutAllOtherSessions={signOutAllOtherSessions}
254264
disabled={!!signingOutDeviceIds.length}
255265
/>
256266
}
@@ -280,6 +290,7 @@ const SessionManagerTab: React.FC = () => {
280290
setPushNotifications={setPushNotifications}
281291
ref={filteredDeviceListRef}
282292
supportsMSC3881={supportsMSC3881}
293+
disableMultipleSignout={disableMultipleSignout}
283294
/>
284295
</SettingsSubsection>
285296
)}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
Copyright 2023 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { IClientWellKnown, IDelegatedAuthConfig, M_AUTHENTICATION } from "matrix-js-sdk/src/matrix";
18+
19+
/**
20+
* Get the delegated auth account management url if configured
21+
* @param clientWellKnown from MatrixClient.getClientWellKnown
22+
* @returns the account management url, or undefined
23+
*/
24+
export const getDelegatedAuthAccountUrl = (clientWellKnown: IClientWellKnown | undefined): string | undefined => {
25+
const delegatedAuthConfig = M_AUTHENTICATION.findIn<IDelegatedAuthConfig | undefined>(clientWellKnown);
26+
return delegatedAuthConfig?.account;
27+
};

0 commit comments

Comments
 (0)