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

@ -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", () => {

View file

@ -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"