From f690925b745c4b5f6c9eedcbe06ea155b0741d4b Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 29 Apr 2024 21:52:34 -0400 Subject: [PATCH 1/6] remove old pre-join UTD logic and add a playwright test for new pre-join UTD --- playwright/e2e/crypto/crypto.spec.ts | 122 ++++++++++++++++++ src/components/structures/TimelinePanel.tsx | 132 ++------------------ src/components/views/rooms/HistoryTile.tsx | 3 - src/i18n/strings/en_EN.json | 1 - 4 files changed, 133 insertions(+), 125 deletions(-) diff --git a/playwright/e2e/crypto/crypto.spec.ts b/playwright/e2e/crypto/crypto.spec.ts index 323e1eb7034..3bd52077ac0 100644 --- a/playwright/e2e/crypto/crypto.spec.ts +++ b/playwright/e2e/crypto/crypto.spec.ts @@ -15,6 +15,7 @@ limitations under the License. */ import type { Page } from "@playwright/test"; +import type { EmittedEvents, Preset } from "matrix-js-sdk/src/matrix"; import { expect, test } from "../../element-web-test"; import { copyAndContinue, @@ -31,6 +32,7 @@ import { import { Bot } from "../../pages/bot"; import { ElementAppPage } from "../../pages/ElementAppPage"; import { Client } from "../../pages/client"; +import { isDendrite } from "../../plugins/homeserver/dendrite"; const openRoomInfo = async (page: Page) => { await page.getByRole("button", { name: "Room info" }).click(); @@ -599,5 +601,125 @@ test.describe("Cryptography", function () { await expect(tilesAfterVerify[1]).toContainText("test2 test2"); await expect(tilesAfterVerify[1].locator(".mx_EventTile_e2eIcon_normal")).toBeVisible(); }); + + test.describe("decryption failure messages", () => { + test.skip(isDendrite, "does not yet support membership on events"); + test.use({ + startHomeserverOpts: "membership-on-events", + }); + + test("should handle non-joined historical messages", async ({ + homeserver, + page, + app, + credentials: aliceCredentials, + user: alice, + cryptoBackend, + bot: bob, + }) => { + test.skip(cryptoBackend === "legacy", "Not implemented for legacy crypto"); + + // Bob creates an encrypted room and sends a message to it. He then invites Alice + const roomId = await bob.evaluate( + async (client, { alice }) => { + const encryptionStatePromise = new Promise((resolve) => { + client.on("RoomState.events" as EmittedEvents, (event, _state, _lastStateEvent) => { + if (event.getType() === "m.room.encryption") { + resolve(); + } + }); + }); + + const { room_id: roomId } = await client.createRoom({ + initial_state: [ + { + type: "m.room.encryption", + content: { + algorithm: "m.megolm.v1.aes-sha2", + }, + }, + ], + name: "Test room", + preset: "private_chat" as Preset, + }); + + // wait for m.room.encryption event, so that when we send a + // message, it will be encrypted + await encryptionStatePromise; + + await client.sendTextMessage(roomId, "This should be undecryptable"); + + await client.invite(roomId, alice.userId); + + return roomId; + }, + { alice }, + ); + + // Alice accepts the invite + await expect( + page.getByRole("group", { name: "Invites" }).locator(".mx_RoomSublist_tiles").getByRole("treeitem"), + ).toHaveCount(1); + await page.getByRole("treeitem", { name: "Test room" }).click(); + await page.locator(".mx_RoomView").getByRole("button", { name: "Accept" }).click(); + + // Bob sends an encrypted event and an undecryptable event + const lastEventId = await bob.evaluate( + async (client, { roomId }) => { + await client.sendTextMessage(roomId, "This should be decryptable"); + const { event_id: lastEventId } = await client.sendEvent( + roomId, + "m.room.encrypted" as any, + { + algorithm: "m.megolm.v1.aes-sha2", + ciphertext: "this+message+will+be+undecryptable", + device_id: client.getDeviceId()!, + sender_key: (await client.getCrypto()!.getOwnDeviceKeys()).ed25519, + session_id: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + } as any, + ); + return lastEventId; + }, + { roomId }, + ); + + // We wait for the event tiles that we expect from the messages that + // Bob sent, in sequence. + await expect( + page.locator(`.mx_EventTile`).getByText("You don't have access to this message"), + ).toBeVisible(); + await expect( + page.locator(`.mx_EventTile`).getByText("This should be decryptable"), + ).toBeVisible(); + await expect( + page.locator(`.mx_EventTile`).getByText("Unable to decrypt message"), + ).toBeVisible(); + + // And then we ensure that they are where we expect them to be + // Alice should see these event tiles: + // - first message sent by Bob (undecryptable) + // - Bob invited Alice + // - Alice joined the room + // - second message sent by Bob (decryptable) + // - third message sent by Bob (undecryptable) + const tiles = await page.locator(".mx_EventTile").all(); + expect(tiles.length).toBeGreaterThanOrEqual(5); + + // The first message from Bob was sent before Alice was in the room, so should + // be different from the standard UTD message + await expect(tiles[tiles.length - 5]).toContainText("You don't have access to this message"); + await expect(tiles[tiles.length - 5].locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible(); + + // The second message from Bob should be decryptable + await expect(tiles[tiles.length - 2]).toContainText("This should be decryptable"); + // this tile won't have an e2e icon since we got the key from the sender + + // The third message from Bob is undecryptable, but was sent while Alice was + // in the room and is expected to be decryptable, so this should have the + // standard UTD message + await expect(tiles[tiles.length - 1]).toContainText("Unable to decrypt message"); + await expect(tiles[tiles.length - 1].locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible(); + }); + }); }); }); diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index 288c65972f1..45198fff740 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -39,8 +39,7 @@ import { ThreadEvent, ReceiptType, } from "matrix-js-sdk/src/matrix"; -import { KnownMembership, Membership } from "matrix-js-sdk/src/types"; -import { debounce, findLastIndex, throttle } from "lodash"; +import { debounce, findLastIndex } from "lodash"; import { logger } from "matrix-js-sdk/src/logger"; import SettingsStore from "../../settings/SettingsStore"; @@ -176,9 +175,6 @@ interface IState { // track whether our room timeline is loading timelineLoading: boolean; - // the index of the first event that is to be shown - firstVisibleEventIndex: number; - // canBackPaginate == false may mean: // // * we haven't (successfully) loaded the timeline yet, or: @@ -296,7 +292,6 @@ class TimelinePanel extends React.Component { events: [], liveEvents: [], timelineLoading: true, - firstVisibleEventIndex: 0, canBackPaginate: false, canForwardPaginate: false, readMarkerVisible: true, @@ -568,12 +563,11 @@ class TimelinePanel extends React.Component { this.overlayTimelineWindow!.unpaginate(overlayCount, backwards); } - const { events, liveEvents, firstVisibleEventIndex } = this.getEvents(); + const { events, liveEvents } = this.getEvents(); this.buildLegacyCallEventGroupers(events); this.setState({ events, liveEvents, - firstVisibleEventIndex, }); // We can now paginate in the unpaginated direction @@ -617,11 +611,6 @@ class TimelinePanel extends React.Component { return Promise.resolve(false); } - if (backwards && this.state.firstVisibleEventIndex !== 0) { - debuglog("won't", dir, "paginate past first visible event"); - return Promise.resolve(false); - } - debuglog("Initiating paginate; backwards:" + backwards); this.setState({ [paginatingKey]: true } as Pick); @@ -636,15 +625,14 @@ class TimelinePanel extends React.Component { debuglog("paginate complete backwards:" + backwards + "; success:" + r); - const { events, liveEvents, firstVisibleEventIndex } = this.getEvents(); + const { events, liveEvents } = this.getEvents(); this.buildLegacyCallEventGroupers(events); const newState = { [paginatingKey]: false, [canPaginateKey]: r, events, liveEvents, - firstVisibleEventIndex, - } as Pick; + } as Pick; // moving the window in this direction may mean that we can now // paginate in the other where we previously could not. @@ -662,11 +650,9 @@ class TimelinePanel extends React.Component { // itself into the right place return new Promise((resolve) => { this.setState(newState, () => { - // we can continue paginating in the given direction if: - // - timelineWindow.paginate says we can - // - we're paginating forwards, or we won't be trying to - // paginate backwards past the first visible event - resolve(r && (!backwards || firstVisibleEventIndex === 0)); + // we can continue paginating in the given direction if + // timelineWindow.paginate says we can + resolve(r); }); }); }); @@ -782,14 +768,13 @@ class TimelinePanel extends React.Component { return; } - const { events, liveEvents, firstVisibleEventIndex } = this.getEvents(); + const { events, liveEvents } = this.getEvents(); this.buildLegacyCallEventGroupers(events); const lastLiveEvent = liveEvents[liveEvents.length - 1]; const updatedState: Partial = { events, liveEvents, - firstVisibleEventIndex, }; let callRMUpdated = false; @@ -967,8 +952,6 @@ class TimelinePanel extends React.Component { if (!this.state.events.includes(ev)) return; - this.recheckFirstVisibleEventIndex(); - // Need to update as we don't display event tiles for events that // haven't yet been decrypted. The event will have just been updated // in place so we just need to re-render. @@ -984,17 +967,6 @@ class TimelinePanel extends React.Component { this.setState({ clientSyncState }); }; - private recheckFirstVisibleEventIndex = throttle( - (): void => { - const firstVisibleEventIndex = this.checkForPreJoinUISI(this.state.events); - if (firstVisibleEventIndex !== this.state.firstVisibleEventIndex) { - this.setState({ firstVisibleEventIndex }); - } - }, - 500, - { leading: true, trailing: true }, - ); - private readMarkerTimeout(readMarkerPosition: number | null): number { return readMarkerPosition === 0 ? this.context?.readMarkerInViewThresholdMs ?? this.state.readMarkerInViewThresholdMs @@ -1721,7 +1693,7 @@ class TimelinePanel extends React.Component { } // get the list of events from the timeline windows and the pending event list - private getEvents(): Pick { + private getEvents(): Pick { const mainEvents = this.timelineWindow!.getEvents(); let overlayEvents = this.overlayTimelineWindow?.getEvents() ?? []; if (this.props.overlayTimelineSetFilter !== undefined) { @@ -1759,8 +1731,6 @@ class TimelinePanel extends React.Component { client.decryptEventIfNeeded(events[i]); } - const firstVisibleEventIndex = this.checkForPreJoinUISI(events); - // Hold onto the live events separately. The read receipt and read marker // should use this list, so that they don't advance into pending events. const liveEvents = [...events]; @@ -1788,87 +1758,9 @@ class TimelinePanel extends React.Component { return { events, liveEvents, - firstVisibleEventIndex, }; } - /** - * Check for undecryptable messages that were sent while the user was not in - * the room. - * - * @param {Array} events The timeline events to check - * - * @return {Number} The index within `events` of the event after the most recent - * undecryptable event that was sent while the user was not in the room. If no - * such events were found, then it returns 0. - */ - private checkForPreJoinUISI(events: MatrixEvent[]): number { - const cli = MatrixClientPeg.safeGet(); - const room = this.props.timelineSet.room; - - const isThreadTimeline = [TimelineRenderingType.Thread, TimelineRenderingType.ThreadsList].includes( - this.context.timelineRenderingType, - ); - if (events.length === 0 || !room || !cli.isRoomEncrypted(room.roomId) || isThreadTimeline) { - logger.debug("checkForPreJoinUISI: showing all messages, skipping check"); - return 0; - } - - const userId = cli.getSafeUserId(); - - // get the user's membership at the last event by getting the timeline - // that the event belongs to, and traversing the timeline looking for - // that event, while keeping track of the user's membership - let i = events.length - 1; - let userMembership: Membership = KnownMembership.Leave; - for (; i >= 0; i--) { - const timeline = this.props.timelineSet.getTimelineForEvent(events[i].getId()!); - if (!timeline) { - // Somehow, it seems to be possible for live events to not have - // a timeline, even though that should not happen. :( - // https://github.com/vector-im/element-web/issues/12120 - logger.warn( - `Event ${events[i].getId()} in room ${room.roomId} is live, ` + `but it does not have a timeline`, - ); - continue; - } - - userMembership = - timeline.getState(EventTimeline.FORWARDS)?.getMember(userId)?.membership ?? KnownMembership.Leave; - const timelineEvents = timeline.getEvents(); - for (let j = timelineEvents.length - 1; j >= 0; j--) { - const event = timelineEvents[j]; - if (event.getId() === events[i].getId()) { - break; - } else if (event.getStateKey() === userId && event.getType() === EventType.RoomMember) { - userMembership = event.getPrevContent().membership || KnownMembership.Leave; - } - } - break; - } - - // now go through the rest of the events and find the first undecryptable - // one that was sent when the user wasn't in the room - for (; i >= 0; i--) { - const event = events[i]; - if (event.getStateKey() === userId && event.getType() === EventType.RoomMember) { - userMembership = event.getPrevContent().membership || KnownMembership.Leave; - } else if ( - userMembership === KnownMembership.Leave && - (event.isDecryptionFailure() || event.isBeingDecrypted()) - ) { - // reached an undecryptable message when the user wasn't in the room -- don't try to load any more - // Note: for now, we assume that events that are being decrypted are - // not decryptable - we will be called once more when it is decrypted. - logger.debug("checkForPreJoinUISI: reached a pre-join UISI at index ", i); - return i + 1; - } - } - - logger.debug("checkForPreJoinUISI: did not find pre-join UISI"); - return 0; - } - private indexForEventId(evId: string | null): number | null { if (evId === null) { return null; @@ -2119,9 +2011,7 @@ class TimelinePanel extends React.Component { // the HS and fetch the latest events, so we are effectively forward paginating. const forwardPaginating = this.state.forwardPaginating || ["PREPARED", "CATCHUP"].includes(this.state.clientSyncState!); - const events = this.state.firstVisibleEventIndex - ? this.state.events.slice(this.state.firstVisibleEventIndex) - : this.state.events; + const events = this.state.events; return ( { highlightedEventId={this.props.highlightedEventId} readMarkerEventId={this.state.readMarkerEventId} readMarkerVisible={this.state.readMarkerVisible} - canBackPaginate={this.state.canBackPaginate && this.state.firstVisibleEventIndex === 0} + canBackPaginate={this.state.canBackPaginate} showUrlPreview={this.props.showUrlPreview} showReadReceipts={this.props.showReadReceipts} ourUserId={MatrixClientPeg.safeGet().getSafeUserId()} diff --git a/src/components/views/rooms/HistoryTile.tsx b/src/components/views/rooms/HistoryTile.tsx index 38449ba9220..42faf0db3e1 100644 --- a/src/components/views/rooms/HistoryTile.tsx +++ b/src/components/views/rooms/HistoryTile.tsx @@ -25,7 +25,6 @@ const HistoryTile: React.FC = () => { const { room } = useContext(RoomContext); const oldState = room?.getLiveTimeline().getState(EventTimeline.BACKWARDS); - const encryptionState = oldState?.getStateEvents("m.room.encryption")[0]; const historyState = oldState?.getStateEvents("m.room.history_visibility")[0]?.getContent().history_visibility; let subtitle: string | undefined; @@ -33,8 +32,6 @@ const HistoryTile: React.FC = () => { subtitle = _t("timeline|no_permission_messages_before_invite"); } else if (historyState == "joined") { subtitle = _t("timeline|no_permission_messages_before_join"); - } else if (encryptionState) { - subtitle = _t("timeline|encrypted_historical_messages_unavailable"); } return ( diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 816f909ec31..b4646c2394e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -3225,7 +3225,6 @@ "tooltip_sub": "Click to view edits", "tooltip_title": "Edited at %(date)s" }, - "encrypted_historical_messages_unavailable": "Encrypted messages before this point are unavailable.", "error_no_renderer": "This event could not be displayed", "error_rendering_message": "Can't load this message", "historical_messages_unavailable": "You can't see earlier messages", From cf5e697845b0df69b0d69615e5441448828b08c2 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 29 Apr 2024 21:59:30 -0400 Subject: [PATCH 2/6] remove variable that isn't used any more --- playwright/e2e/crypto/crypto.spec.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/playwright/e2e/crypto/crypto.spec.ts b/playwright/e2e/crypto/crypto.spec.ts index 3bd52077ac0..4dad6965573 100644 --- a/playwright/e2e/crypto/crypto.spec.ts +++ b/playwright/e2e/crypto/crypto.spec.ts @@ -664,7 +664,7 @@ test.describe("Cryptography", function () { await page.locator(".mx_RoomView").getByRole("button", { name: "Accept" }).click(); // Bob sends an encrypted event and an undecryptable event - const lastEventId = await bob.evaluate( + await bob.evaluate( async (client, { roomId }) => { await client.sendTextMessage(roomId, "This should be decryptable"); const { event_id: lastEventId } = await client.sendEvent( @@ -678,7 +678,6 @@ test.describe("Cryptography", function () { session_id: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", } as any, ); - return lastEventId; }, { roomId }, ); From 3971fb896701e621c162e0a53063f4517f483f80 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 30 Apr 2024 10:04:41 -0400 Subject: [PATCH 3/6] add missing synapse template --- .../templates/membership-on-events/README.md | 1 + .../membership-on-events/homeserver.yaml | 101 ++++++++++++++++++ .../templates/membership-on-events/log.config | 50 +++++++++ 3 files changed, 152 insertions(+) create mode 100644 playwright/plugins/homeserver/synapse/templates/membership-on-events/README.md create mode 100644 playwright/plugins/homeserver/synapse/templates/membership-on-events/homeserver.yaml create mode 100644 playwright/plugins/homeserver/synapse/templates/membership-on-events/log.config diff --git a/playwright/plugins/homeserver/synapse/templates/membership-on-events/README.md b/playwright/plugins/homeserver/synapse/templates/membership-on-events/README.md new file mode 100644 index 00000000000..e7a71b26f6e --- /dev/null +++ b/playwright/plugins/homeserver/synapse/templates/membership-on-events/README.md @@ -0,0 +1 @@ +A synapse configured with MSC4115 (user membership in events) support diff --git a/playwright/plugins/homeserver/synapse/templates/membership-on-events/homeserver.yaml b/playwright/plugins/homeserver/synapse/templates/membership-on-events/homeserver.yaml new file mode 100644 index 00000000000..dcfc71e3bda --- /dev/null +++ b/playwright/plugins/homeserver/synapse/templates/membership-on-events/homeserver.yaml @@ -0,0 +1,101 @@ +server_name: "localhost" +pid_file: /data/homeserver.pid +public_baseurl: "{{PUBLIC_BASEURL}}" +listeners: + - port: 8008 + tls: false + bind_addresses: ["::"] + type: http + x_forwarded: true + + resources: + - names: [client] + compress: false + +database: + name: "sqlite3" + args: + database: ":memory:" + +log_config: "/data/log.config" + +rc_messages_per_second: 10000 +rc_message_burst_count: 10000 +rc_registration: + per_second: 10000 + burst_count: 10000 +rc_joins: + local: + per_second: 9999 + burst_count: 9999 + remote: + per_second: 9999 + burst_count: 9999 +rc_joins_per_room: + per_second: 9999 + burst_count: 9999 +rc_3pid_validation: + per_second: 1000 + burst_count: 1000 + +rc_invites: + per_room: + per_second: 1000 + burst_count: 1000 + per_user: + per_second: 1000 + burst_count: 1000 + +rc_login: + address: + per_second: 10000 + burst_count: 10000 + account: + per_second: 10000 + burst_count: 10000 + failed_attempts: + per_second: 10000 + burst_count: 10000 + +media_store_path: "/data/media_store" +uploads_path: "/data/uploads" +enable_registration: true +enable_registration_without_verification: true +disable_msisdn_registration: false +registration_shared_secret: "{{REGISTRATION_SECRET}}" +report_stats: false +macaroon_secret_key: "{{MACAROON_SECRET_KEY}}" +form_secret: "{{FORM_SECRET}}" +signing_key_path: "/data/localhost.signing.key" + +trusted_key_servers: + - server_name: "matrix.org" +suppress_key_server_warning: true + +ui_auth: + session_timeout: "300s" + +oidc_providers: + - idp_id: test + idp_name: "OAuth test" + issuer: "http://localhost:{{OAUTH_SERVER_PORT}}/oauth" + authorization_endpoint: "http://localhost:{{OAUTH_SERVER_PORT}}/oauth/auth.html" + # the token endpoint receives requests from synapse, rather than the webapp, so needs to escape the docker container. + token_endpoint: "http://host.containers.internal:{{OAUTH_SERVER_PORT}}/oauth/token" + userinfo_endpoint: "http://host.containers.internal:{{OAUTH_SERVER_PORT}}/oauth/userinfo" + client_id: "synapse" + discover: false + scopes: ["profile"] + skip_verification: true + client_auth_method: none + user_mapping_provider: + config: + display_name_template: "{{ user.name }}" + +# Inhibit background updates as this Synapse isn't long-lived +background_updates: + min_batch_size: 100000 + sleep_duration_ms: 100000 + +experimental_features: + msc4115_membership_on_events: true diff --git a/playwright/plugins/homeserver/synapse/templates/membership-on-events/log.config b/playwright/plugins/homeserver/synapse/templates/membership-on-events/log.config new file mode 100644 index 00000000000..b9123d0f5b9 --- /dev/null +++ b/playwright/plugins/homeserver/synapse/templates/membership-on-events/log.config @@ -0,0 +1,50 @@ +# Log configuration for Synapse. +# +# This is a YAML file containing a standard Python logging configuration +# dictionary. See [1] for details on the valid settings. +# +# Synapse also supports structured logging for machine readable logs which can +# be ingested by ELK stacks. See [2] for details. +# +# [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema +# [2]: https://matrix-org.github.io/synapse/latest/structured_logging.html + +version: 1 + +formatters: + precise: + format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s' + +handlers: + # A handler that writes logs to stderr. Unused by default, but can be used + # instead of "buffer" and "file" in the logger handlers. + console: + class: logging.StreamHandler + formatter: precise + +loggers: + synapse.storage.SQL: + # beware: increasing this to DEBUG will make synapse log sensitive + # information such as access tokens. + level: DEBUG + + twisted: + # We send the twisted logging directly to the file handler, + # to work around https://github.com/matrix-org/synapse/issues/3471 + # when using "buffer" logger. Use "console" to log to stderr instead. + handlers: [console] + propagate: false + +root: + level: DEBUG + + # Write logs to the `buffer` handler, which will buffer them together in memory, + # then write them to a file. + # + # Replace "buffer" with "console" to log to stderr instead. (Note that you'll + # also need to update the configuration for the `twisted` logger above, in + # this case.) + # + handlers: [console] + +disable_existing_loggers: false From d9221ba275e4a1d6494ac5f99b0025705650b911 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 30 Apr 2024 15:16:53 -0400 Subject: [PATCH 4/6] remove unused variable (again) and run prettier --- playwright/e2e/crypto/crypto.spec.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/playwright/e2e/crypto/crypto.spec.ts b/playwright/e2e/crypto/crypto.spec.ts index 4dad6965573..a555c1dd250 100644 --- a/playwright/e2e/crypto/crypto.spec.ts +++ b/playwright/e2e/crypto/crypto.spec.ts @@ -667,7 +667,7 @@ test.describe("Cryptography", function () { await bob.evaluate( async (client, { roomId }) => { await client.sendTextMessage(roomId, "This should be decryptable"); - const { event_id: lastEventId } = await client.sendEvent( + await client.sendEvent( roomId, "m.room.encrypted" as any, { @@ -687,12 +687,8 @@ test.describe("Cryptography", function () { await expect( page.locator(`.mx_EventTile`).getByText("You don't have access to this message"), ).toBeVisible(); - await expect( - page.locator(`.mx_EventTile`).getByText("This should be decryptable"), - ).toBeVisible(); - await expect( - page.locator(`.mx_EventTile`).getByText("Unable to decrypt message"), - ).toBeVisible(); + await expect(page.locator(`.mx_EventTile`).getByText("This should be decryptable")).toBeVisible(); + await expect(page.locator(`.mx_EventTile`).getByText("Unable to decrypt message")).toBeVisible(); // And then we ensure that they are where we expect them to be // Alice should see these event tiles: From 23af83b499a4d40568522a5da53a72d3d82540b9 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Fri, 3 May 2024 17:09:05 -0400 Subject: [PATCH 5/6] add test that we can jump to an event before our latest join membership event --- playwright/e2e/crypto/crypto.spec.ts | 106 ++++++++++++++++++++++++++- 1 file changed, 103 insertions(+), 3 deletions(-) diff --git a/playwright/e2e/crypto/crypto.spec.ts b/playwright/e2e/crypto/crypto.spec.ts index a555c1dd250..35decb4947d 100644 --- a/playwright/e2e/crypto/crypto.spec.ts +++ b/playwright/e2e/crypto/crypto.spec.ts @@ -1,5 +1,5 @@ /* -Copyright 2022 The Matrix.org Foundation C.I.C. +Copyright 2022-2024 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. @@ -602,13 +602,13 @@ test.describe("Cryptography", function () { await expect(tilesAfterVerify[1].locator(".mx_EventTile_e2eIcon_normal")).toBeVisible(); }); - test.describe("decryption failure messages", () => { + test.describe("non-joined historical messages", () => { test.skip(isDendrite, "does not yet support membership on events"); test.use({ startHomeserverOpts: "membership-on-events", }); - test("should handle non-joined historical messages", async ({ + test("should display undecryptable non-joined historical messages with a different message", async ({ homeserver, page, app, @@ -715,6 +715,106 @@ test.describe("Cryptography", function () { await expect(tiles[tiles.length - 1]).toContainText("Unable to decrypt message"); await expect(tiles[tiles.length - 1].locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible(); }); + + test("should be able to jump to a message sent before our last join event", async ({ + homeserver, + page, + app, + credentials: aliceCredentials, + user: alice, + cryptoBackend, + bot: bob, + }) => { + // The old pre-join UTD hiding code would hide events sent + // before our latest join event, even if the event that we're + // jumping to was decryptable. We test that this no longer happens. + + test.skip(cryptoBackend === "legacy", "Not implemented for legacy crypto"); + + // Bob: + // - creates an encrypted room, + // - invites Alice, + // - sends a message to it, + // - kicks Alice, + // - sends a bunch more events + // - invites Alice again + // In this way, there will be an event that Alice can decrypt, + // followed by a bunch of undecryptable events which Alice shouldn't + // expect to be able to decrypt. The old code would have hidden all + // the events, even the decryptable event (which it wouldn't have + // even tried to fetch, if it was far enough back). + const { roomId, eventId } = await bob.evaluate( + async (client, { alice }) => { + const { room_id: roomId } = await client.createRoom({ + initial_state: [ + { + type: "m.room.encryption", + content: { + algorithm: "m.megolm.v1.aes-sha2", + }, + }, + ], + name: "Test room", + preset: "private_chat" as Preset, + }); + + // invite Alice + const inviteAlicePromise = new Promise((resolve) => { + client.on("RoomMember.membership" as EmittedEvents, (_event, member, _oldMembership?) => { + if (member.userId === alice.userId && member.membership === "invite") { + resolve(); + } + }); + }); + await client.invite(roomId, alice.userId); + // wait for the invite to come back so that we encrypt to Alice + await inviteAlicePromise; + + // send a message that Alice should be able to decrypt + const { event_id: eventId } = await client.sendTextMessage( + roomId, + "This should be decryptable", + ); + + // kick Alice + const kickAlicePromise = new Promise((resolve) => { + client.on("RoomMember.membership" as EmittedEvents, (_event, member, _oldMembership?) => { + if (member.userId === alice.userId && member.membership === "leave") { + resolve(); + } + }); + }); + await client.kick(roomId, alice.userId); + await kickAlicePromise; + + // send a bunch of messages that Alice won't be able to decrypt + for (let i = 0; i < 20; i++) { + await client.sendTextMessage(roomId, `${i}`); + } + + // invite Alice again + await client.invite(roomId, alice.userId); + + return { roomId, eventId }; + }, + { alice }, + ); + + // Alice accepts the invite + await expect( + page.getByRole("group", { name: "Invites" }).locator(".mx_RoomSublist_tiles").getByRole("treeitem"), + ).toHaveCount(1); + await page.getByRole("treeitem", { name: "Test room" }).click(); + await page.locator(".mx_RoomView").getByRole("button", { name: "Accept" }).click(); + + // wait until we're joined and see the timeline + await expect(page.locator(`.mx_EventTile`).getByText("Alice joined the room")).toBeVisible(); + + // we should be able to jump to the decryptable message that Bob sent + await page.goto(`#/room/${roomId}/${eventId}`); + + await expect(page.locator(`.mx_EventTile`).getByText("This should be decryptable")).toBeVisible(); + }); }); }); }); From 9a0ed2ee85ead806e8c38f20d3daa988dbc02c42 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Fri, 10 May 2024 15:34:25 -0400 Subject: [PATCH 6/6] modify default template instead of creating a new template --- playwright/e2e/crypto/crypto.spec.ts | 3 - .../synapse/templates/default/homeserver.yaml | 6 ++ .../templates/membership-on-events/README.md | 1 - .../membership-on-events/homeserver.yaml | 101 ------------------ .../templates/membership-on-events/log.config | 50 --------- 5 files changed, 6 insertions(+), 155 deletions(-) delete mode 100644 playwright/plugins/homeserver/synapse/templates/membership-on-events/README.md delete mode 100644 playwright/plugins/homeserver/synapse/templates/membership-on-events/homeserver.yaml delete mode 100644 playwright/plugins/homeserver/synapse/templates/membership-on-events/log.config diff --git a/playwright/e2e/crypto/crypto.spec.ts b/playwright/e2e/crypto/crypto.spec.ts index 35decb4947d..326aeaff8e7 100644 --- a/playwright/e2e/crypto/crypto.spec.ts +++ b/playwright/e2e/crypto/crypto.spec.ts @@ -604,9 +604,6 @@ test.describe("Cryptography", function () { test.describe("non-joined historical messages", () => { test.skip(isDendrite, "does not yet support membership on events"); - test.use({ - startHomeserverOpts: "membership-on-events", - }); test("should display undecryptable non-joined historical messages with a different message", async ({ homeserver, diff --git a/playwright/plugins/homeserver/synapse/templates/default/homeserver.yaml b/playwright/plugins/homeserver/synapse/templates/default/homeserver.yaml index c5bea307b44..bc3ecd7c9b5 100644 --- a/playwright/plugins/homeserver/synapse/templates/default/homeserver.yaml +++ b/playwright/plugins/homeserver/synapse/templates/default/homeserver.yaml @@ -96,3 +96,9 @@ oidc_providers: background_updates: min_batch_size: 100000 sleep_duration_ms: 100000 + +experimental_features: + # Needed for e2e/crypto/crypto.spec.ts > Cryptography > decryption failure + # messages > non-joined historical messages. + # Can be removed after Synapse enables it by default + msc4115_membership_on_events: true diff --git a/playwright/plugins/homeserver/synapse/templates/membership-on-events/README.md b/playwright/plugins/homeserver/synapse/templates/membership-on-events/README.md deleted file mode 100644 index e7a71b26f6e..00000000000 --- a/playwright/plugins/homeserver/synapse/templates/membership-on-events/README.md +++ /dev/null @@ -1 +0,0 @@ -A synapse configured with MSC4115 (user membership in events) support diff --git a/playwright/plugins/homeserver/synapse/templates/membership-on-events/homeserver.yaml b/playwright/plugins/homeserver/synapse/templates/membership-on-events/homeserver.yaml deleted file mode 100644 index dcfc71e3bda..00000000000 --- a/playwright/plugins/homeserver/synapse/templates/membership-on-events/homeserver.yaml +++ /dev/null @@ -1,101 +0,0 @@ -server_name: "localhost" -pid_file: /data/homeserver.pid -public_baseurl: "{{PUBLIC_BASEURL}}" -listeners: - - port: 8008 - tls: false - bind_addresses: ["::"] - type: http - x_forwarded: true - - resources: - - names: [client] - compress: false - -database: - name: "sqlite3" - args: - database: ":memory:" - -log_config: "/data/log.config" - -rc_messages_per_second: 10000 -rc_message_burst_count: 10000 -rc_registration: - per_second: 10000 - burst_count: 10000 -rc_joins: - local: - per_second: 9999 - burst_count: 9999 - remote: - per_second: 9999 - burst_count: 9999 -rc_joins_per_room: - per_second: 9999 - burst_count: 9999 -rc_3pid_validation: - per_second: 1000 - burst_count: 1000 - -rc_invites: - per_room: - per_second: 1000 - burst_count: 1000 - per_user: - per_second: 1000 - burst_count: 1000 - -rc_login: - address: - per_second: 10000 - burst_count: 10000 - account: - per_second: 10000 - burst_count: 10000 - failed_attempts: - per_second: 10000 - burst_count: 10000 - -media_store_path: "/data/media_store" -uploads_path: "/data/uploads" -enable_registration: true -enable_registration_without_verification: true -disable_msisdn_registration: false -registration_shared_secret: "{{REGISTRATION_SECRET}}" -report_stats: false -macaroon_secret_key: "{{MACAROON_SECRET_KEY}}" -form_secret: "{{FORM_SECRET}}" -signing_key_path: "/data/localhost.signing.key" - -trusted_key_servers: - - server_name: "matrix.org" -suppress_key_server_warning: true - -ui_auth: - session_timeout: "300s" - -oidc_providers: - - idp_id: test - idp_name: "OAuth test" - issuer: "http://localhost:{{OAUTH_SERVER_PORT}}/oauth" - authorization_endpoint: "http://localhost:{{OAUTH_SERVER_PORT}}/oauth/auth.html" - # the token endpoint receives requests from synapse, rather than the webapp, so needs to escape the docker container. - token_endpoint: "http://host.containers.internal:{{OAUTH_SERVER_PORT}}/oauth/token" - userinfo_endpoint: "http://host.containers.internal:{{OAUTH_SERVER_PORT}}/oauth/userinfo" - client_id: "synapse" - discover: false - scopes: ["profile"] - skip_verification: true - client_auth_method: none - user_mapping_provider: - config: - display_name_template: "{{ user.name }}" - -# Inhibit background updates as this Synapse isn't long-lived -background_updates: - min_batch_size: 100000 - sleep_duration_ms: 100000 - -experimental_features: - msc4115_membership_on_events: true diff --git a/playwright/plugins/homeserver/synapse/templates/membership-on-events/log.config b/playwright/plugins/homeserver/synapse/templates/membership-on-events/log.config deleted file mode 100644 index b9123d0f5b9..00000000000 --- a/playwright/plugins/homeserver/synapse/templates/membership-on-events/log.config +++ /dev/null @@ -1,50 +0,0 @@ -# Log configuration for Synapse. -# -# This is a YAML file containing a standard Python logging configuration -# dictionary. See [1] for details on the valid settings. -# -# Synapse also supports structured logging for machine readable logs which can -# be ingested by ELK stacks. See [2] for details. -# -# [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema -# [2]: https://matrix-org.github.io/synapse/latest/structured_logging.html - -version: 1 - -formatters: - precise: - format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s' - -handlers: - # A handler that writes logs to stderr. Unused by default, but can be used - # instead of "buffer" and "file" in the logger handlers. - console: - class: logging.StreamHandler - formatter: precise - -loggers: - synapse.storage.SQL: - # beware: increasing this to DEBUG will make synapse log sensitive - # information such as access tokens. - level: DEBUG - - twisted: - # We send the twisted logging directly to the file handler, - # to work around https://github.com/matrix-org/synapse/issues/3471 - # when using "buffer" logger. Use "console" to log to stderr instead. - handlers: [console] - propagate: false - -root: - level: DEBUG - - # Write logs to the `buffer` handler, which will buffer them together in memory, - # then write them to a file. - # - # Replace "buffer" with "console" to log to stderr instead. (Note that you'll - # also need to update the configuration for the `twisted` logger above, in - # this case.) - # - handlers: [console] - -disable_existing_loggers: false