Skip to content

Commit

Permalink
Merge pull request #172 from tchapgouv/111-allow-external-user-in-pri…
Browse files Browse the repository at this point in the history
…vate-room

Allow external users from private room
  • Loading branch information
estellecomment authored Sep 28, 2022
2 parents 0f5b11e + eccdcef commit f7b475d
Show file tree
Hide file tree
Showing 7 changed files with 194 additions and 16 deletions.
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);
}
}
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 TchapIAccessRuleEventContent {
rule: TchapRoomAccessRule; // eslint-disable-line camelcase
}

export const TchapRoomAccessRulesEventId = "im.vector.room.access_rules";
48 changes: 46 additions & 2 deletions src/components/views/settings/TchapJoinRuleSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,11 @@ import { ROOM_SECURITY_TAB } from "matrix-react-sdk/src/components/views/dialogs
import { Action } from "matrix-react-sdk/src/dispatcher/actions";
import { ViewRoomPayload } from "matrix-react-sdk/src/dispatcher/payloads/ViewRoomPayload";
import { doesRoomVersionSupport, PreferredRoomVersions } from "matrix-react-sdk/src/utils/PreferredRoomVersions";
import LabelledToggleSwitch from "matrix-react-sdk/src/components/views/elements/LabelledToggleSwitch";
import QuestionDialog from "matrix-react-sdk/src/components/views/dialogs/QuestionDialog";

import TchapUIFeature from "../../../util/TchapUIFeature";
import { TchapRoomAccessRule, TchapIAccessRuleEventContent, TchapRoomAccessRulesEventId } from "../../../@types/tchap";

interface IProps {
room: Room;
Expand All @@ -63,12 +66,18 @@ 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 [contentTchapAccessRule, setTchapAccessRule] = useLocalEcho<TchapIAccessRuleEventContent>(
() => room.currentState.getStateEvents(TchapRoomAccessRulesEventId, "")?.getContent(),
content => cli.sendStateEvent(room.roomId, TchapRoomAccessRulesEventId, content, ""),
onError,
);
const { rule: accessRule = undefined } = contentTchapAccessRule || {};

const editRestrictedRoomIds = async (): Promise<string[] | undefined> => {
let selected = restrictedAllowRoomIds;
if (!selected?.length && SpaceStore.instance.activeSpaceRoom) {
Expand Down Expand Up @@ -104,11 +113,46 @@ const JoinRuleSettings = ({ room, promptUpgrade, aliasWarning, onError, beforeCh
// :TCHAP: we do not permit to change the type of room, thus display only one option
const definitions: IDefinition<JoinRule>[] = [];

// :TCHAP: do we need to add the following condition as well (joinRule === JoinRule.Restricted && !restrictedAllowRoomIds?.length)?
if (joinRule === JoinRule.Invite) {
let privateRoomDescription = <div>
{ _t("Only invited people can join.") }
</div>;
// :TCHAP: We could add functions in 'TchapUtils' to determine the type of room and rely on this logic to display components as we did in Android :
// :TCHAP: https://github.com/tchapgouv/tchap-android/blob/develop/vector/src/main/java/fr/gouv/tchap/core/utils/RoomUtils.kt#L31
if (accessRule) {
const openedToExternalUsers = accessRule === TchapRoomAccessRule.Unrestricted;
const onExternalAccessChange = async () => {
Modal.createDialog(QuestionDialog, {
title: _t("Allow external users 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;
setTchapAccessRule({ "rule": TchapRoomAccessRule.Unrestricted });
},
});
};
privateRoomDescription = <div>
<div>
{ _t("Only invited people can join.") }
</div>
<span>
<LabelledToggleSwitch
value={openedToExternalUsers}
onChange={onExternalAccessChange}
label={_t("Allow external users 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
16 changes: 16 additions & 0 deletions src/i18n/strings/tchap_translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,5 +88,21 @@
"We have compiled a list of the questions our users ask the most frequently. Your question may be answered there.": {
"en": "We have compiled a list of the questions our users ask the most frequently. Your question may be answered there.",
"fr": "Nous avons constitué une liste des question les plus fréquentes posées par nos utilisateurs. Votre question a peut-être déjà une solution."
},
"Allow external users to join this room": {
"en": "Allow external users to join this room",
"fr": "Autoriser les externes à rejoindre ce salon"
},
"This action is irreversible.": {
"en": "This action is irreversible.",
"fr": "Cette action est irréversible."
},
"Are you sure you want to allow the externals to join this room ?": {
"en": "Are you sure you want to allow the externals to join this room ?",
"fr": "Voulez-vous vraiment autoriser l’accès aux externes à ce salon ?"
},
"Create Room": {
"en": "Create New Room",
"fr": "Créer un nouveau salon"
}
}
37 changes: 28 additions & 9 deletions src/lib/createTchapRoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,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, TchapRoomAccessRulesEventId, TchapRoomType } from "../@types/tchap";

export const DEFAULT_FEDERATE_VALUE = true;

Expand All @@ -23,41 +19,64 @@ 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;
// Here we could have used createRoomOpts.accessRule directly,
// but since accessRules are a custom Tchap event, it is ignored by later code.
// So we use createRoomOpts.initial_state, which works properly.
createRoomOpts.initial_state.push({
content: {
rule: TchapRoomAccessRule.Restricted,
},
type: TchapRoomAccessRulesEventId,
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: TchapRoomAccessRulesEventId,
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: TchapRoomAccessRulesEventId,
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, TchapRoomAccessRulesEventId } 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: TchapRoomAccessRulesEventId, 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 external users 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 external users 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

0 comments on commit f7b475d

Please sign in to comment.