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:
Kerry 2023-08-22 14:25:34 +12:00 committed by GitHub
parent 3c52ba0c92
commit dfded8d4d3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 362 additions and 61 deletions

View file

@ -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)}

View file

@ -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 })

View file

@ -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>
);
};

View file

@ -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 });
}

View file

@ -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>
)}