Skip to content

Commit

Permalink
Enables E2EE for the Bot (#456)
Browse files Browse the repository at this point in the history
  • Loading branch information
mgcm authored Feb 21, 2024
1 parent 3b567a3 commit 075c456
Show file tree
Hide file tree
Showing 37 changed files with 562 additions and 123 deletions.
5 changes: 5 additions & 0 deletions .changeset/fast-dogs-love.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@nordeck/matrix-meetings-bot': minor
---

Enables encryption support for the Bot
1 change: 0 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,5 @@
!/matrix-meetings-bot/package.json
!/matrix-meetings-bot/conf
!/matrix-meetings-bot/lib
!/resolutions/matrix-sdk-crypto-nodejs
!/packages/calendar/package.json
!/packages/calendar/lib
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ on:
jobs:
build-widget:
runs-on: ubuntu-latest
timeout-minutes: 25
timeout-minutes: 30
env:
DOCKER_IMAGE: ghcr.io/nordeck/matrix-meetings-widget
steps:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ while [ $(curl -k -sw '%{http_code}' "$HOMESERVER" -o /dev/null) -ne 302 ]; do
done
response=$(curl -k --write-out '%{http_code}' --silent --output /dev/null -X GET --header 'Accept: application/json' $HOMESERVER/_matrix/client/r0/register/available?username=$USERTOCREATE)
if [ "$response" = 400 ]; then
echo "User already existant"
echo "Bot user already exists"
else
echo "Will create User $USERTOCREATE on $HOMESERVER"
register_new_matrix_user -a -u $USERTOCREATE -p `cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1` -c /data/homeserver.yaml $HOMESERVER
register_new_matrix_user -a -u $USERTOCREATE -p $BOT_PASSWORD -c /data/homeserver.yaml $HOMESERVER
fi
exit 0
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
#/bin/sh

TOKEN=$(psql -X -A -w -t -c "select token from access_tokens where user_id='@$USERTOCREATE:$SERVER'")
echo "ACCESS_TOKEN=$TOKEN" > /work-dir/.env
# Get the login token
TOKEN_RESPONSE=$(curl -s -X POST -H "Content-Type: application/json" -d '{"type":"m.login.password","user":"'${USERTOCREATE}'","password":"'${BOT_PASSWORD}'"}' "${HOMESERVER}/_matrix/client/r0/login")

# Extract the access token from the response
ACCESS_TOKEN=$(echo $TOKEN_RESPONSE | grep -o '"access_token":"[^"]*' | cut -d'"' -f4)

if [ "$ACCESS_TOKEN" != "null" ]; then
echo "Login successful. Access token: $ACCESS_TOKEN"
else
echo "Login failed. Check your credentials and try again."
fi

# Add it to the env file so it can be used by the bot
echo "ACCESS_TOKEN=$ACCESS_TOKEN" > /work-dir/.env
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#/bin/sh

USER=$(psql -X -A -w -t -c "select user_id from ratelimit_override where user_id='@$USERTOCREATE:$SERVER'")
USER=$(psql -X -A -w -t -c "select user_id from ratelimit_override where user_id='@$USERTOCREATE:$SERVER'")
if [ "$USER" = 400 ]; then
echo "Limit is already set"
exit 0
Expand Down
43 changes: 14 additions & 29 deletions charts/matrix-meetings-bot/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ spec:
value: "{{ .Values.init.username }}"
- name: HOMESERVER
value: "{{ .Values.init.homeserverUrl }}"
- name: BOT_PASSWORD
valueFrom:
secretKeyRef:
name: meetings-bot-credentials
key: password
command:
- sh
- /scripts/create_bot_account.sh
Expand Down Expand Up @@ -89,7 +94,7 @@ spec:
- name: SERVER
value: "{{ .Values.init.homeserver }}"
{{- end }}
{{- if .Values.init.readBotTokenFromDB.enabled }}
{{- if .Values.init.getFreshDeviceToken.enabled }}
- name: getbottoken
image: {{ .Values.init.postgresClient.image }}
command:
Expand All @@ -102,35 +107,15 @@ spec:
- name: shell-tools
mountPath: /scripts
env:
- name: PGPORT
valueFrom:
secretKeyRef:
name: pg-credentials
key: db_port
- name: PGPASSWORD
- name: USERTOCREATE
value: "{{ .Values.init.username }}"
- name: HOMESERVER
value: "{{ .Values.init.homeserverUrl }}"
- name: BOT_PASSWORD
valueFrom:
secretKeyRef:
name: pg-credentials
name: meetings-bot-credentials
key: password
- name: PGDATABASE
valueFrom:
secretKeyRef:
name: pg-credentials
key: db_name
- name: PGUSER
valueFrom:
secretKeyRef:
name: pg-credentials
key: username
- name: PGHOST
valueFrom:
secretKeyRef:
name: pg-credentials
key: db_host
- name: USERTOCREATE
value: "{{ .Values.init.username }}"
- name: SERVER
value: "{{ .Values.init.homeserver }}"
{{- end }}
containers:
- name: {{ .Chart.Name }}
Expand Down Expand Up @@ -165,7 +150,7 @@ spec:
volumeMounts:
- name: data
mountPath: /app/storage
{{- if .Values.init.readBotTokenFromDB.enabled }}
{{- if .Values.init.getFreshDeviceToken.enabled }}
- name: workdir
mountPath: "/app/.env"
subPath: ".env"
Expand Down Expand Up @@ -196,7 +181,7 @@ spec:
configMap:
name: {{ include "matrix-meetings-bot.fullname" . }}-cm
defaultMode: 0777
{{- if or .Values.init.createUserAccount.enabled .Values.init.disableRateLimitInDB.enabled .Values.init.readBotTokenFromDB.enabled }}
{{- if or .Values.init.createUserAccount.enabled .Values.init.disableRateLimitInDB.enabled .Values.init.getFreshDeviceToken.enabled }}
- name: shell-tools
configMap:
name: {{ include "matrix-meetings-bot.fullname" . }}-sh-tools
Expand Down
4 changes: 2 additions & 2 deletions charts/matrix-meetings-bot/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ settings:
# - name: HOMESERVER_URL
# value: 'https://matrix-client.matrix.org'

## Configure the access token (can be skipped if init.readBotTokenFromDB.enabled is activated)
## Configure the access token (can be skipped if init.getFreshDeviceToken.enabled is activated)
# - name: ACCESS_TOKEN
# secretKeyRef:
# name: pg-credentials
Expand Down Expand Up @@ -137,5 +137,5 @@ init:
disableRateLimitInDB:
enabled: false

readBotTokenFromDB:
getFreshDeviceToken:
enabled: false
2 changes: 2 additions & 0 deletions charts/matrix-meetings/values.dev.yaml.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,5 @@ matrix-meetings-bot:
value: 'info'
- name: AUTO_DELETION_OFFSET
value: '60'
- name: ENABLE_CRYPTO
value: 'true'
6 changes: 6 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@ MATRIX_SERVER_EVENT_MAX_AGE_MINUTES=5
# optional - the folder where the bot stores local data like a persisted sessions
STORAGE_FILE_DATA_PATH=storage

# optional - enables the bot to create end-to-end encrypted rooms for meetings and control rooms
ENABLE_CRYPTO=false

# optional - the folder where the bot stores local encryption data. This is relative to the storage path above, ie 'storage/crypto'
CRYPTO_DATA_PATH=crypto

# optional - the json file with the session information inside the storage folder
STORAGE_FILE_FILENAME=bot.json

Expand Down
2 changes: 1 addition & 1 deletion e2e/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { devices } from '@playwright/test';
const config: PlaywrightTestConfig = {
testDir: './src',
/* Increase default timeout from 30 sec as we often scratch it. */
timeout: 60000,
timeout: 90000,
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
Expand Down
1 change: 1 addition & 0 deletions e2e/src/deploy/matrixMeetingsBot/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export async function startMatrixMeetingsBot({
'https://webmail-hostname/appsuite/#app=io.ox/calendar&id={{id}}&folder={{folder}}',
AUTO_DELETION_OFFSET: '60',
ENABLE_GUEST_USER_POWER_LEVEL_CHANGE: 'true',
ENABLE_CRYPTO: 'true',
LOG_LEVEL: 'debug',
})
.withCopyFilesToContainer([
Expand Down
202 changes: 202 additions & 0 deletions e2e/src/e2eeMeetingRoom.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
/*
* Copyright 2024 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 { expect } from '@playwright/test';
import { test } from './fixtures';

test.describe('Encrypted Meeting Room', () => {
test.beforeEach(
async ({
bob,
aliceEncryptedMeetingsWidgetPage,
aliceElementWebPage,
aliceJitsiWidgetPage,
}) => {
test.setTimeout(60000);

await aliceEncryptedMeetingsWidgetPage.setDateFilter(
[2040, 10, 1],
[2040, 10, 8],
);

const aliceScheduleMeetingWidgetPage =
await aliceEncryptedMeetingsWidgetPage.scheduleMeeting();

await aliceScheduleMeetingWidgetPage.titleTextbox.fill('My Meeting');
await aliceScheduleMeetingWidgetPage.descriptionTextbox.fill(
'My Description',
);
await aliceScheduleMeetingWidgetPage.setStart([2040, 10, 3], '10:30 AM');
await aliceScheduleMeetingWidgetPage.addParticipant(bob.displayName);
await aliceScheduleMeetingWidgetPage.submit();

await aliceElementWebPage.inviteUser(bob.username);

await aliceElementWebPage.waitForRoomJoin('My Meeting');
await aliceEncryptedMeetingsWidgetPage
.getMeeting('My Meeting', '10/03/2040')
.joinMeeting();
await aliceJitsiWidgetPage.joinConferenceButton.waitFor();
},
);

test('should have jitsi, breakout sessions, and settings widget setup in the room', async ({
aliceElementWebPage,
aliceCockpitWidgetPage,
}) => {
expect(await aliceElementWebPage.getWidgets()).toEqual([
'Breakout Sessions',
'NeoDateFix Details',
'Video Conference',
]);

await expect(aliceElementWebPage.roomNameText).toHaveText('My Meeting');
await expect(aliceElementWebPage.roomTopicText).toHaveText(
'My Description',
);

await aliceElementWebPage.showWidgetInSidebar('NeoDateFix Details');
const meetingDetails = aliceCockpitWidgetPage.getMeeting();
await aliceElementWebPage.approveWidgetIdentity();
await expect(meetingDetails.meetingDescriptionText).toHaveText(
'My Description',
);
await expect(meetingDetails.meetingTitleText).toHaveText('My Meeting');
await expect(meetingDetails.meetingTimeRangeText).toHaveText(
'October 3, 2040, 10:30 – 11:30 AM',
);
});

test('should edit the meeting title from within the meeting', async ({
aliceElementWebPage,
aliceCockpitWidgetPage,
}) => {
await aliceElementWebPage.showWidgetInSidebar('NeoDateFix Details');

const meetingDetails = aliceCockpitWidgetPage.getMeeting();
await aliceElementWebPage.approveWidgetIdentity();
const aliceEditMeetingWidgetPage = await meetingDetails.editMeeting();
await aliceEditMeetingWidgetPage.titleTextbox.fill('New Meeting');
await aliceEditMeetingWidgetPage.submit();

await expect(meetingDetails.meetingTitleText).toHaveText('New Meeting');
await expect(aliceElementWebPage.roomNameText).toHaveText('New Meeting');
await expect(
aliceElementWebPage.locateChatMessageInRoom(/Title: New Meeting/),
).toBeVisible();
});

// eslint-disable-next-line playwright/expect-expect
test('should add the meeting participant from within the meeting', async ({
aliceElementWebPage,
aliceCockpitWidgetPage,
charlie,
}) => {
await aliceElementWebPage.showWidgetInSidebar('NeoDateFix Details');

const meetingDetails = aliceCockpitWidgetPage.getMeeting();
await aliceElementWebPage.approveWidgetIdentity();

const aliceEditMeetingWidgetPage = await meetingDetails.editMeeting();
await aliceEditMeetingWidgetPage.addParticipant(charlie.displayName);
await aliceEditMeetingWidgetPage.submit();

await aliceElementWebPage.waitForUserMembership(charlie.username, 'invite');
});

test('should disable the video conference from within the meeting', async ({
aliceElementWebPage,
aliceCockpitWidgetPage,
}) => {
await aliceElementWebPage.showWidgetInSidebar('NeoDateFix Details');
const meetingDetails = aliceCockpitWidgetPage.getMeeting();
await aliceElementWebPage.approveWidgetIdentity();
const aliceEditMeetingWidgetPage = await meetingDetails.editMeeting();
await aliceEditMeetingWidgetPage.removeLastWidget();
await aliceEditMeetingWidgetPage.submit();

await aliceElementWebPage
.locateChatMessageInRoom('Video conference ended by Bot')
.waitFor();

await aliceElementWebPage.closeWidgetInSidebar();

await expect
.poll(async () => {
return await aliceElementWebPage.getWidgets();
})
.toEqual(['Breakout Sessions', 'NeoDateFix Details']);
});

test('should enable the optional widget from within the meeting', async ({
aliceElementWebPage,
aliceCockpitWidgetPage,
}) => {
await aliceElementWebPage.showWidgetInSidebar('NeoDateFix Details');
const meetingDetails = aliceCockpitWidgetPage.getMeeting();
await aliceElementWebPage.approveWidgetIdentity();

const aliceEditMeetingWidgetPage = await meetingDetails.editMeeting();
await aliceEditMeetingWidgetPage.addWidget('Video Conference (optional)');
await aliceEditMeetingWidgetPage.submit();

await expect
.poll(async () => {
return await aliceElementWebPage.getWidgets();
})
.toEqual([
'Breakout Sessions',
'NeoDateFix Details',
'Video Conference',
'Video Conference (optional)',
]);
});

test('should toggle whether users can use the chat', async ({
aliceElementWebPage,
aliceCockpitWidgetPage,
bobElementWebPage,
bobMeetingsWidgetPage,
}) => {
await bobElementWebPage.navigateToRoomOrInvitation('Calendar');
await bobElementWebPage.acceptRoomInvitation();
await bobElementWebPage.approveWidgetWarning();
await bobElementWebPage.approveWidgetCapabilities();

await bobMeetingsWidgetPage.setDateFilter([2040, 10, 1], [2040, 10, 8]);

await bobMeetingsWidgetPage
.getMeeting('My Meeting', '10/03/2040')
.joinMeeting();
await bobElementWebPage.acceptRoomInvitation();
await bobElementWebPage.sendMessage('I am Bob');
await aliceElementWebPage.sendMessage('I am Alice');
await aliceElementWebPage.showWidgetInSidebar('NeoDateFix Details');
const meetingCard = aliceCockpitWidgetPage.getMeeting();
await aliceElementWebPage.approveWidgetIdentity();
let aliceEditMeetingWidgetPage = await meetingCard.editMeeting();
await aliceEditMeetingWidgetPage.toggleChatPermission();
await aliceEditMeetingWidgetPage.submit();
await aliceElementWebPage.sendMessage('I am still here');
await expect(bobElementWebPage.noChatPermissionText).toBeVisible();
aliceEditMeetingWidgetPage = await meetingCard.editMeeting();
await aliceEditMeetingWidgetPage.toggleChatPermission();
await aliceEditMeetingWidgetPage.submit();
await expect(bobElementWebPage.noChatPermissionText).toBeHidden();
await bobElementWebPage.sendMessage('I am Bob again');
await aliceElementWebPage.sendMessage('I am Alice again');
});
});
Loading

0 comments on commit 075c456

Please sign in to comment.