Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow external users from private room #172

Merged
merged 16 commits into from
Sep 28, 2022
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 43 additions & 4 deletions cypress/e2e/room-access-settings/room-access-settings.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/// <reference types="cypress" />

import RoomUtils from "../utils/room-utils";
import RandomUtils from "../utils/random-utils";

describe("Check room access settings", () => {
const homeserverUrl = Cypress.env('E2E_TEST_USER_HOMESERVER_URL');
Expand All @@ -17,7 +18,7 @@ describe("Check room access settings", () => {
});

it("creates a public room and check access settings", () => {
const roomName = "test/"+today+"/public_room_check_access_settings";
const roomName = "test/"+today+"/public_room_check_access_settings"+RandomUtils.generateRandom(4);

RoomUtils.createPublicRoom(roomName)
.then((roomId) => {
Expand All @@ -43,7 +44,7 @@ describe("Check room access settings", () => {
});

it("creates a private room and check access settings", () => {
const roomName = "test/"+today+"/private_room_check_access_settings";
const roomName = "test/"+today+"/private_room_check_access_settings"+RandomUtils.generateRandom(4);

RoomUtils.createPrivateRoom(roomName)
.then((roomId) => {
Expand All @@ -60,12 +61,18 @@ describe("Check room access settings", () => {
cy.get('.mx_AccessibleButton').should('have.attr', 'aria-disabled', 'true');
});

//external user access switch should not be on and not disabled
cy.contains(".mx_SettingsFlag", /^Autoriser les externes à rejoindre ce salon$/).within(() => {
cy.get('.mx_AccessibleButton').should('have.attr', 'aria-checked', 'false');
cy.get('.mx_AccessibleButton').should('have.attr', 'aria-disabled', 'false');
});

cy.leaveRoom(roomId);
});
});

it("creates a private room with external and check access settings", () => {
const roomName = "test/"+today+"/external_room_check_access_settings";
const roomName = "test/"+today+"/external_room_check_access_settings"+RandomUtils.generateRandom(4);

RoomUtils.createPrivateWithExternalRoom(roomName)
.then((roomId) => {
Expand All @@ -82,7 +89,39 @@ describe("Check room access settings", () => {
cy.get('.mx_AccessibleButton').should('have.attr', 'aria-disabled', 'true');
});

cy.leaveRoom(roomId);
// external user access switch should be on and disabled
cy.contains(".mx_SettingsFlag", /^Autoriser les externes à rejoindre ce salon$/).within(() => {
cy.get('.mx_AccessibleButton').should('have.attr', 'aria-checked', 'true');
cy.get('.mx_AccessibleButton').should('have.attr', 'aria-disabled', 'true');
});

// NOTE : unfortunately we cannot leave a room where access have been give to external users
// TODO : we need to find a way to remove a private room with external access (Admin API)
// cy.leaveRoom(roomId);
});
});

it("allow access for external users on a private room", () => {
const roomName = "test/"+today+"/private_room_change_external_access_settings"+RandomUtils.generateRandom(4);

RoomUtils.createPrivateRoom(roomName)
.then((roomId) => {
RoomUtils.openRoomAccessSettings(roomName);

// click on 'Allow the externals to join' this room
cy.get('[aria-label="Autoriser les externes à rejoindre ce salon"]').click();
// click on the confirmation popup box
cy.get('[data-test-id="dialog-primary-button"]').click();

//assert
cy.contains(".mx_SettingsFlag", /^Autoriser les externes à rejoindre ce salon$/).within(() => {
cy.get('.mx_AccessibleButton').should('have.attr', 'aria-checked', 'true');
cy.get('.mx_AccessibleButton').should('have.attr', 'aria-disabled', 'true');
});

// NOTE : unfortunately we cannot leave a room where access have been give to external users
// TODO : we need to find a way to remove a private room with external access (Admin API)
// cy.leaveRoom(roomId);
});
});
});
Expand Down
10 changes: 10 additions & 0 deletions cypress/e2e/utils/random-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export default class RandomUtils {
/**
* Generate a random string to a length of max 10 char
* @param length < 10
* @returns random string
*/
static generateRandom(length: number): string {
return (Math.random() + 1).toString(36).substring(2, 2+length);
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
"test:cypress:open": "cypress open",
"coverage": "yarn test --coverage",
"analyse:unused-exports": "node ./scripts/analyse_unused_exports.js",
"postinstall": "patch-package"
"postinstall": "scripts/apply_patches.sh"
},
"dependencies": {
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.12.tgz",
Expand Down
10 changes: 10 additions & 0 deletions scripts/apply_patches.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/bin/sh
#
# Script to apply the patches in patches directory, using patch-package.
set -e

for d in patches/*/ ; do
echo "Patching $d..."
yarn patch-package --patch-dir "$d"
echo "...$d done."
done
6 changes: 6 additions & 0 deletions src/@types/tchap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,9 @@ export enum TchapRoomAccessRule {
Unrestricted = "unrestricted", // accessible to externals
Restricted = "restricted" // not accessible to externals
}

export interface IAccessRuleEventContent {
mcalinghee marked this conversation as resolved.
Show resolved Hide resolved
rule: TchapRoomAccessRule; // eslint-disable-line camelcase
}

export const RoomAccessRulesEventId = "im.vector.room.access_rules";
47 changes: 45 additions & 2 deletions src/components/views/settings/TchapJoinRuleSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ import { doesRoomVersionSupport, PreferredRoomVersions } from "matrix-react-sdk/

import TchapUIFeature from "../../../util/TchapUIFeature";

import { TchapRoomAccessRule, IAccessRuleEventContent, RoomAccessRulesEventId } from "../../../@types/tchap";
import LabelledToggleSwitch from "matrix-react-sdk/src/components/views/elements/LabelledToggleSwitch";
import QuestionDialog from "matrix-react-sdk/src/components/views/dialogs/QuestionDialog";

interface IProps {
room: Room;
promptUpgrade?: boolean;
Expand All @@ -63,12 +67,19 @@ const JoinRuleSettings = ({ room, promptUpgrade, aliasWarning, onError, beforeCh
content => cli.sendStateEvent(room.roomId, EventType.RoomJoinRules, content, ""),
onError,
);

const { join_rule: joinRule = JoinRule.Invite } = content || {};
const restrictedAllowRoomIds = joinRule === JoinRule.Restricted
? content.allow?.filter(o => o.type === RestrictedAllowType.RoomMembership).map(o => o.room_id)
: undefined;


const [contentAccessRule, setContentAccess] = useLocalEcho<IAccessRuleEventContent>(
mcalinghee marked this conversation as resolved.
Show resolved Hide resolved
() => room.currentState.getStateEvents(RoomAccessRulesEventId, "")?.getContent(),
content => cli.sendStateEvent(room.roomId, RoomAccessRulesEventId, content, ""),
onError,
);
const { rule: accessRule = undefined } = contentAccessRule || {};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pourquoi le default undefined, sachant qu'on a deja || {} ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cela permet d'assigner accessRule à undefined si contentAccessRule est lui aussi `undefined

Si on enleve {}, on peut avoir l'erreur suivante : Error: Cannot read properties of undefined (reading 'rule')


const editRestrictedRoomIds = async (): Promise<string[] | undefined> => {
let selected = restrictedAllowRoomIds;
if (!selected?.length && SpaceStore.instance.activeSpaceRoom) {
Expand Down Expand Up @@ -105,10 +116,42 @@ const JoinRuleSettings = ({ room, promptUpgrade, aliasWarning, onError, beforeCh
const definitions: IDefinition<JoinRule>[] = [];

if (joinRule === JoinRule.Invite) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this selecting private rooms only ? So far we were are copying android's implementation for determining what type of room it is :
https://github.com/tchapgouv/tchap-android/blob/develop/vector/src/main/java/fr/gouv/tchap/core/utils/RoomUtils.kt#L31
(selection on isEncrypted and accessRules)

It would be nice to have a utils function (in TchapUtils for example) that checks what type of room it is.
v2 had a isRoomForum function for example, we could do something similar

let privateRoomDescription = <div>
{ _t("Only invited people can join.") }
</div>;
if (accessRule) {
const openedToExternalUsers = accessRule === TchapRoomAccessRule.Unrestricted;
const onExternalAccessChange = async () => {
Modal.createDialog(QuestionDialog, {
title: _t("Allow the externals to join this room"),
description: _t('This action is irreversible.') + " "
+ _t('Are you sure you want to allow the externals to join this room ?'),
button: _t("OK"),
onFinished: (confirmed) => {
if (!confirmed) return;
setContentAccess({ "rule": TchapRoomAccessRule.Unrestricted });
},
});
};
privateRoomDescription = <div>
<div>
{ _t("Only invited people can join.") }
</div>
<span>
<LabelledToggleSwitch
value={openedToExternalUsers}
onChange={onExternalAccessChange}
label={_t("Allow the externals to join this room")}
disabled={disabled || openedToExternalUsers}
/>
</span>
</div>;
}

definitions.push({
value: JoinRule.Invite,
label: _t("Private (invite only)"),
description: _t("Only invited people can join."),
description: privateRoomDescription,
checked: true,
});
} else if (joinRule === JoinRule.Public) {
Expand Down
5 changes: 4 additions & 1 deletion src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,8 @@
"Decentralised, encrypted chat &amp; collaboration powered by $matrixLogo": "Decentralised, encrypted chat &amp; collaboration powered by $matrixLogo",
"Sign In": "Sign In",
"Create Account": "Create Account",
"Explore rooms": "Explore rooms"
"Explore rooms": "Explore rooms",
mcalinghee marked this conversation as resolved.
Show resolved Hide resolved
"Allow the externals to join this room": "Allow the externals to join this room",
mcalinghee marked this conversation as resolved.
Show resolved Hide resolved
"This action is irreversible.": "This action is irreversible.",
"Are you sure you want to allow the externals to join this room ?": "Are you sure you want to allow the externals to join this room ?"
}
5 changes: 4 additions & 1 deletion src/i18n/strings/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,8 @@
"%(brand)s uses advanced browser features which aren't supported by your current browser.": "%(brand)s nécessite des fonctionnalités avancées que votre navigateur actuel ne prend pas en charge.",
"Powered by Matrix": "Propulsé par Matrix",
"Use %(brand)s on mobile": "Utiliser %(brand)s sur téléphone",
"Decentralised, encrypted chat &amp; collaboration powered by $matrixLogo": "Messagerie décentralisée, chiffrée &amp; une collaboration alimentée par $matrixLogo"
"Decentralised, encrypted chat &amp; collaboration powered by $matrixLogo": "Messagerie décentralisée, chiffrée &amp; une collaboration alimentée par $matrixLogo",
"Allow the externals to join this room": "Autoriser les externes à rejoindre ce salon",
"This action is irreversible.": "Cette action est irréversible.",
"Are you sure you want to allow the externals to join this room ?": "Voulez-vous vraiment autoriser l’accès aux externes à ce salon ?"
}
35 changes: 25 additions & 10 deletions src/lib/createTchapRoom.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import { IOpts } from "matrix-react-sdk/src/createRoom";
import { ICreateRoomOpts } from "matrix-js-sdk/src/@types/requests";
import { HistoryVisibility, JoinRule, Preset, Visibility } from "matrix-js-sdk/src/@types/partials";

import { TchapRoomAccessRule, TchapRoomType } from "../@types/tchap";

export interface ITchapCreateRoomOpts extends ICreateRoomOpts {
accessRule?: TchapRoomAccessRule;
}
import { TchapRoomAccessRule, RoomAccessRulesEventId, TchapRoomType } from "../@types/tchap";

export const DEFAULT_FEDERATE_VALUE = true;

Expand All @@ -23,41 +18,61 @@ export default class TchapCreateRoom {
tchapRoomType: TchapRoomType,
federate: boolean = DEFAULT_FEDERATE_VALUE): IOpts {
const opts: IOpts = {};
const createRoomOpts: ITchapCreateRoomOpts = {};
const createRoomOpts: ICreateRoomOpts = {};
opts.createOpts = createRoomOpts;

//tchap common options
createRoomOpts.name = name;
opts.guestAccess = false; //guest access are not authorized in tchap

createRoomOpts.creation_content = { 'm.federate': federate };
createRoomOpts.initial_state = createRoomOpts.initial_state || [];

switch (tchapRoomType) {
case TchapRoomType.Forum: {
//"Forum" only for tchap members and not encrypted
createRoomOpts.accessRule = TchapRoomAccessRule.Restricted;
createRoomOpts.visibility = Visibility.Public;
createRoomOpts.preset = Preset.PublicChat;
createRoomOpts.initial_state.push({
mcalinghee marked this conversation as resolved.
Show resolved Hide resolved
mcalinghee marked this conversation as resolved.
Show resolved Hide resolved
content: {
rule: TchapRoomAccessRule.Restricted,
},
type: RoomAccessRulesEventId,
state_key: '',
});

opts.joinRule = JoinRule.Public;
opts.encryption = false;
opts.historyVisibility = HistoryVisibility.Shared;
break;
}
case TchapRoomType.Private: {
//"Salon", only for tchap member and encrypted
createRoomOpts.accessRule = TchapRoomAccessRule.Restricted;
createRoomOpts.visibility = Visibility.Private;
createRoomOpts.preset = Preset.PrivateChat;
createRoomOpts.initial_state.push({
content: {
rule: TchapRoomAccessRule.Restricted,
},
type: RoomAccessRulesEventId,
state_key: '',
});
opts.joinRule = JoinRule.Invite;
opts.encryption = true;
opts.historyVisibility = HistoryVisibility.Invited;
break;
}
case TchapRoomType.External: {
//open to external and encrypted,
createRoomOpts.accessRule = TchapRoomAccessRule.Unrestricted;
createRoomOpts.visibility = Visibility.Private;
createRoomOpts.preset = Preset.PrivateChat;
createRoomOpts.initial_state.push({
content: {
rule: TchapRoomAccessRule.Unrestricted,
},
type: RoomAccessRulesEventId,
state_key: '',
});
opts.joinRule = JoinRule.Invite;
opts.encryption = true;
opts.historyVisibility = HistoryVisibility.Invited;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { mocked } from 'jest-mock';

import TchapJoinRuleSettings from "../../../../../src/components/views/settings/TchapJoinRuleSettings";
import { createTestClient, mkStubRoom } from "matrix-react-sdk/test/test-utils/test-utils";
import { createTestClient, mkStubRoom, mockStateEventImplementation, mkEvent } from "matrix-react-sdk/test/test-utils/test-utils";
import { JoinRule, MatrixClient, Room } from "matrix-js-sdk/src/matrix";
import { TchapRoomAccessRule, RoomAccessRulesEventId } from "../../../../../src/@types/tchap";



Expand All @@ -14,6 +16,23 @@ function mkStubRoomWithInviteRule(roomId: string, name: string, client: MatrixCl
return stubRoom;
}

const makeAccessEvent = (rule: TchapRoomAccessRule = TchapRoomAccessRule.Restricted) => mkEvent({
type: RoomAccessRulesEventId, event: true, content: {
rule: rule,
},
} as any);

function mkStubRoomWithAccessRule(roomId: string, name: string, client: MatrixClient, joinRule: JoinRule, accessRule: TchapRoomAccessRule): Room {
const stubRoom:Room = mkStubRoom(roomId,name,client);
stubRoom.getJoinRule = jest.fn().mockReturnValue(joinRule);
stubRoom.currentState.getJoinRule = jest.fn().mockReturnValue(joinRule);
const events = [
makeAccessEvent(accessRule),
];
mocked(stubRoom.currentState).getStateEvents.mockImplementation(mockStateEventImplementation(events));
return stubRoom;
}

describe("TchapJoinRule", () => {


Expand All @@ -31,12 +50,37 @@ describe("TchapJoinRule", () => {
render(<TchapJoinRuleSettings {...props} />);

//assert that spaces option is not here while private and public are
const publicText = "Public"
const privateText = "Private (invite only)"
const allowExternalText = "Allow the externals to join this room"
const spaceText = "Anyone in a space can find and join"

expect(screen.queryByText(publicText)).toBe(null);
expect(screen.queryByText(privateText)).toBeDefined();
expect(screen.queryByText(allowExternalText)).toBe(null);
expect(screen.queryByText(spaceText)).toBe(null);
});

it("should render the tchap join rule with only private option with restricted access rules", () => {
//build stub private room
const props = {
room: mkStubRoomWithAccessRule("roomId", "roomName", createTestClient(), JoinRule.Invite, TchapRoomAccessRule.Restricted),
closeSettingsFn(){},
onError(error: Error){},
}

//arrange
render(<TchapJoinRuleSettings {...props} />);

//assert that spaces option is not here while private and public are
const publicText = "Public"
const privateText = "Private (invite only)"
const allowExternalText = "Allow the externals to join this room"
const spaceText = "Anyone in a space can find and join"

expect(screen.queryByText(publicText)).toBe(null);
expect(screen.queryByText(privateText)).toBeDefined();
expect(screen.queryByText(allowExternalText)).toBeDefined();
expect(screen.queryByText(spaceText)).toBe(null);
});

Expand Down