Skip to content

Commit

Permalink
Merge pull request #7 from nordeck/nic/feat/disable-features-for-guests
Browse files Browse the repository at this point in the history
Disable certain homeserver-wide actions (create room, invite user) for guest users
  • Loading branch information
dhenneke authored Sep 15, 2023
2 parents 02162e0 + b43b066 commit 0cdd42f
Show file tree
Hide file tree
Showing 15 changed files with 393 additions and 3 deletions.
6 changes: 6 additions & 0 deletions .changeset/six-radios-matter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@nordeck/element-web-guest-module': minor
'@nordeck/synapse-guest-module': minor
---

Disable certain homeserver-wide actions (create room, invite user) for guest users.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Guest users...
- ... have a **real user account** on the Homeserver.
- ... get a **username** with the (configurable) pattern `@guest-<random-identifier>`.
- ... have a **display name** that always includes the (configurable) suffix ` (Guest)`.
- ... are **restricted** in what they can do (can't create rooms or participate in direct messages on the homeserver).

## Getting Started

Expand Down
1 change: 1 addition & 0 deletions e2e/src/deploy/elementWeb/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ RUN yarn --network-timeout=200000 install
# Add all configurations
COPY /module.tgz /src
COPY /build_config.yaml /src
COPY /customisations.json /src

# Build Element
RUN bash /src/scripts/docker-package.sh
Expand Down
3 changes: 3 additions & 0 deletions e2e/src/deploy/elementWeb/customisations.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"src/customisations/ComponentVisibility.ts": "node_modules/@nordeck/element-web-guest-module/customisations/ComponentVisibility.ts"
}
61 changes: 61 additions & 0 deletions e2e/src/guestLogin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,4 +126,65 @@ test.describe('Guest Module', () => {
'A new name (Guest)',
);
});

test('should limit UI actions for the guest users', async ({
aliceElementWebPage,
guestElementWebPage,
}) => {
const { roomId } = await aliceElementWebPage.createRoom('My New Room', {
visibility: 'public',
});

await guestElementWebPage.navigateToRoomWithLink(roomId);

const loginFormPage = await guestElementWebPage.openGuestLoginForm();

await loginFormPage.continueAsGuest('My Name');

await guestElementWebPage.waitForRoom('My New Room');

// Alice (i.e. a regular user) can do everything
for (const locator of aliceElementWebPage.getHiddenGuestLocators()) {
await expect(locator).toBeAttached();
}

// The guest misses these elements
for (const locator of guestElementWebPage.getHiddenGuestLocators()) {
await expect(locator).not.toBeAttached();
}
});

test('should deny home server actions for the guest user', async ({
aliceElementWebPage,
guestElementWebPage,
bob,
}) => {
const { roomId } = await aliceElementWebPage.createRoom('My New Room', {
visibility: 'public',
});

await guestElementWebPage.navigateToRoomWithLink(roomId);

const loginFormPage = await guestElementWebPage.openGuestLoginForm();

await loginFormPage.continueAsGuest('My Name');

await guestElementWebPage.waitForRoom('My New Room');

// Use the matrix client since the buttons are already hidden from the UI

await expect(
aliceElementWebPage.createRoom('A new room'),
).resolves.not.toThrow();
await expect(
aliceElementWebPage.inviteUser(bob.username),
).resolves.not.toThrow();

await expect(guestElementWebPage.createRoom('A new room')).rejects.toThrow(
/You are not permitted to create rooms/,
);
await expect(guestElementWebPage.inviteUser(bob.username)).rejects.toThrow(
/Invites have been disabled on this server/,
);
});
});
32 changes: 32 additions & 0 deletions e2e/src/pages/elementWebPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,21 @@ export class ElementWebPage {
.click();
}

async inviteUser(username: string) {
const roomId = this.getCurrentRoomId();

// Instead of controlling the UI, we use the matrix client as it is faster.
await this.page.evaluate(
async ({ roomId, username }) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const client = (window as any).mxMatrixClientPeg.get();

await client.invite(roomId, `@${username}:localhost`);
},
{ roomId, username },
);
}

async login(username: string, password: string): Promise<Credentials> {
const synapseUrl = getSynapseUrl();
const url = `${synapseUrl}/_matrix/client/r0/login`;
Expand Down Expand Up @@ -229,4 +244,21 @@ export class ElementWebPage {

return userSettingsPage;
}

public getHiddenGuestLocators(): Locator[] {
return [
// Controlled by UIComponent.CreateRooms, UIComponent.ExploreRooms
this.navigationRegion.getByRole('button', { name: 'Start chat' }),
this.navigationRegion.getByRole('button', { name: 'Add room' }),

// Controlled by UIComponent.CreateSpaces
this.navigationRegion.getByRole('button', { name: 'Create a space' }),

// Controlled by UIComponent.InviteUsers
this.mainRegion.getByRole('button', { name: 'Invite to this room' }),

// Controlled by UIComponent.RoomOptionsMenu
this.headerRegion.getByRole('button', { name: 'Room options' }),
];
}
}
18 changes: 17 additions & 1 deletion element-web-guest-module/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ modules:
- '@nordeck/element-web-guest-module@^1.0.0'
```
Also create a `customisations.json` file with the following content:

```json
{
"src/customisations/ComponentVisibility.ts": "node_modules/@nordeck/element-web-guest-module/customisations/ComponentVisibility.ts"
}
```

Build Element and deploy your custom version as described by the original documentation.
In case you want to create a docker-based build process, you might find inspiration in the setup [we use for our e2e tests](../e2e/src/deploy/elementWeb/Dockerfile).

Expand Down Expand Up @@ -67,6 +75,14 @@ Example configuration:
- 'file:../element-web-guest-module/element-web-guest-module'
```

4. (In the `element-web` folder) Run `yarn start` and access it at `http://localhost:8080`
4. (In the `element-web` folder) Create a `customisations.json` with the following content:

```json
{
"src/customisations/ComponentVisibility.ts": "node_modules/@nordeck/element-web-guest-module/customisations/ComponentVisibility.ts"
}
```

5. (In the `element-web` folder) Run `yarn start` and access it at `http://localhost:8080`

> **Important**: You must run `yarn build` in this repo and restart Element after each change in the module.
78 changes: 78 additions & 0 deletions element-web-guest-module/customisations/ComponentVisibility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright 2023 Nordeck IT + Consulting GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

// This file is exported as-is. It will be used by the Element build as a
// TypeScript file. We don't actually depend on the matrix-react-sdk, though
// all relevant and testable code is imported from the bundled module.

/* eslint-disable no-undef */
// @ts-nocheck

/*
* This file hides the UI features that are also disabled via the Synapse module.
* This should eventually also be moved to the Module API, see also
* https://github.com/matrix-org/matrix-react-sdk-module-api/pull/12
*/

import {
GUEST_MODULE_CONFIG_KEY,
GUEST_MODULE_CONFIG_NAMESPACE,
GuestModuleConfig,
assertValidGuestModuleConfig,
shouldShowComponent as shouldShowComponentShared,
} from '@nordeck/element-web-guest-module';
import { MatrixClientPeg } from 'matrix-react-sdk/src/MatrixClientPeg';
import SdkConfig from 'matrix-react-sdk/src/SdkConfig';
import { UIComponent } from 'matrix-react-sdk/src/settings/UIFeature';

export function getConfig(): GuestModuleConfig {
const rawConfig = SdkConfig.get(GUEST_MODULE_CONFIG_NAMESPACE)?.[
GUEST_MODULE_CONFIG_KEY
];

assertValidGuestModuleConfig(rawConfig);

return rawConfig;
}

/**
* Determines whether or not the active MatrixClient user should be able to use
* the given UI component. If shown, the user might still not be able to use the
* component depending on their contextual permissions. For example, invite options
* might be shown to the user but they won't have permission to invite users to
* the current room: the button will appear disabled.
* @param {UIComponent} component The component to check visibility for.
* @returns {boolean} True (default) if the user is able to see the component, false
* otherwise.
*/
function shouldShowComponent(component: UIComponent): boolean {
const config = getConfig();
const myUserId = MatrixClientPeg.safeGet().getSafeUserId();

return shouldShowComponentShared(config, myUserId, component);
}

// This interface summarises all available customisation points and also marks
// them all as optional. This allows customisers to only define and export the
// customisations they need while still maintaining type safety.
export interface IComponentVisibilityCustomisations {
shouldShowComponent?: typeof shouldShowComponent;
}

// A real customisation module will define and export one or more of the
// customisation points that make up the interface above.
export const ComponentVisibilityCustomisations: IComponentVisibilityCustomisations =
{ shouldShowComponent };
5 changes: 3 additions & 2 deletions element-web-guest-module/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"tsc": "tsc",
"lint": "eslint . --max-warnings=0",
"test": "jest --watch",
"depcheck": "depcheck",
"depcheck": "depcheck --ignore-patterns='customisations/*'",
"package": "rimraf *.tgz && npm pack"
},
"devDependencies": {
Expand Down Expand Up @@ -50,7 +50,8 @@
"directory": "element-web-guest-module"
},
"files": [
"build"
"build",
"customisations"
],
"keywords": [
"element",
Expand Down
6 changes: 6 additions & 0 deletions element-web-guest-module/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,10 @@
import { GuestModule } from './GuestModule';

export default GuestModule;
export {
GUEST_MODULE_CONFIG_KEY,
GUEST_MODULE_CONFIG_NAMESPACE,
assertValidGuestModuleConfig,
} from './config';
export type { GuestModuleConfig } from './config';
export { shouldShowComponent } from './shouldShowComponent';
63 changes: 63 additions & 0 deletions element-web-guest-module/src/shouldShowComponent.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright 2023 Nordeck IT + Consulting GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { GuestModuleConfig } from './config';
import { shouldShowComponent } from './shouldShowComponent';

describe('shouldShowComponent', () => {
describe.each`
extra_config | test_guest
${{}} | ${'@guest-asdf'}
${{ guest_user_prefix: '@custom-' }} | ${'@custom-asdf'}
`('with $extra_config config', ({ extra_config, test_guest }) => {
let config: GuestModuleConfig;

beforeEach(() => {
config = {
...extra_config,
};
});

it.each([
'UIComponent.sendInvites',
'UIComponent.roomCreation',
'UIComponent.spaceCreation',
'UIComponent.exploreRooms',
'UIComponent.roomOptionsMenu',
])('should reject %s for guests', (component) => {
expect(shouldShowComponent(config, test_guest, component)).toBe(false);
});

it.each(['UIComponent.filterContainer', 'UIComponent.addIntegrations'])(
'should accept %s for guests',
(component) => {
expect(shouldShowComponent(config, test_guest, component)).toBe(true);
},
);

it.each([
'UIComponent.sendInvites',
'UIComponent.roomCreation',
'UIComponent.spaceCreation',
'UIComponent.exploreRooms',
'UIComponent.roomOptionsMenu',
'UIComponent.filterContainer',
'UIComponent.addIntegrations',
])('should accept %s for normal users', (component) => {
expect(shouldShowComponent(config, '@userid', component)).toBe(true);
});
});
});
55 changes: 55 additions & 0 deletions element-web-guest-module/src/shouldShowComponent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 2023 Nordeck IT + Consulting GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { GuestModuleConfig } from './config';

const GUEST_INVISIBLE_COMPONENTS = [
'UIComponent.sendInvites',
'UIComponent.roomCreation',
'UIComponent.spaceCreation',
'UIComponent.exploreRooms',
'UIComponent.roomOptionsMenu',
// TODO: UIComponent.AddIntegrations does hide the whole integrations sidebar,
// where it should only hide the buttons and slash command. If this gets fixed
// in element we can disable it also for guests.
//UIComponent.AddIntegrations,
];

function getGuestUserPrefix(config: GuestModuleConfig): string {
return config.guest_user_prefix ?? '@guest-';
}

/**
* A function that can be called from a `ComponentVisibility` customisation. It
* returns true, if the `userId` should see the `component`.
*
* @param config - the configuration of the module
* @param userId - the id of the user to check
* @param component - the name of the component that is checked
* @returns true, if the user should see the component
*/
export function shouldShowComponent(
config: GuestModuleConfig,
userId: string,
component: string,
): boolean {
const components = userId.startsWith(getGuestUserPrefix(config))
? GUEST_INVISIBLE_COMPONENTS
: [];

const shouldShow = !components.includes(component);
return shouldShow;
}
Loading

0 comments on commit 0cdd42f

Please sign in to comment.