Skip to content

Commit 3039968

Browse files
authored
fix: PDFs uploaded by "PDF transcript" feature were returnig 403 when attempting to download (#32329)
1 parent 1bdffcd commit 3039968

File tree

6 files changed

+136
-30
lines changed

6 files changed

+136
-30
lines changed

.changeset/rude-llamas-notice.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@rocket.chat/meteor": patch
3+
"@rocket.chat/i18n": patch
4+
"@rocket.chat/omnichannel-services": patch
5+
---
6+
7+
Added a new setting `Restrict files access to users who can access room` that controls file visibility. This new setting allows users that "can access a room" to also download the files that are there. This is specially important for users with livechat manager or monitor roles, or agents that have special permissions to view closed rooms, since this allows them to download files on the conversation even after the conversation is closed.
8+
New setting is disabled by default and it is mutually exclusive with the setting `Restrict file access to room members` since this allows _more_ types of users to download files.

apps/meteor/app/file-upload/server/lib/FileUpload.ts

+14-4
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator';
2828
import { UploadFS } from '../../../../server/ufs';
2929
import { ufsComplete } from '../../../../server/ufs/ufs-methods';
3030
import type { Store, StoreOptions } from '../../../../server/ufs/ufs-store';
31-
import { canAccessRoomAsync } from '../../../authorization/server/functions/canAccessRoom';
31+
import { canAccessRoomAsync, canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom';
3232
import { settings } from '../../../settings/server';
3333
import { mime } from '../../../utils/lib/mimeTypes';
3434
import { isValidJWT, generateJWT } from '../../../utils/server/lib/JWTHelper';
@@ -463,16 +463,26 @@ export const FileUpload = {
463463
return false;
464464
}
465465

466-
if (!settings.get('FileUpload_Restrict_to_room_members') || !file?.rid) {
466+
if (!file?.rid) {
467467
return true;
468468
}
469469

470-
const subscription = await Subscriptions.findOneByRoomIdAndUserId(file.rid, user._id, { projection: { _id: 1 } });
470+
const fileUploadRestrictedToMembers = settings.get<boolean>('FileUpload_Restrict_to_room_members');
471+
const fileUploadRestrictToUsersWhoCanAccessRoom = settings.get<boolean>('FileUpload_Restrict_to_users_who_can_access_room');
471472

472-
if (subscription) {
473+
if (!fileUploadRestrictToUsersWhoCanAccessRoom && !fileUploadRestrictedToMembers) {
473474
return true;
474475
}
475476

477+
if (fileUploadRestrictedToMembers && !fileUploadRestrictToUsersWhoCanAccessRoom) {
478+
const sub = await Subscriptions.findOneByRoomIdAndUserId(file.rid, user._id, { projection: { _id: 1 } });
479+
return !!sub;
480+
}
481+
482+
if (fileUploadRestrictToUsersWhoCanAccessRoom && !fileUploadRestrictedToMembers) {
483+
return canAccessRoomIdAsync(file.rid, user._id);
484+
}
485+
476486
return false;
477487
},
478488

apps/meteor/server/settings/file-upload.ts

+24-4
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,30 @@ export const createFileUploadSettings = () =>
3333

3434
await this.add('FileUpload_Restrict_to_room_members', true, {
3535
type: 'boolean',
36-
enableQuery: {
37-
_id: 'FileUpload_ProtectFiles',
38-
value: true,
39-
},
36+
enableQuery: [
37+
{
38+
_id: 'FileUpload_ProtectFiles',
39+
value: true,
40+
},
41+
{
42+
_id: 'FileUpload_Restrict_to_users_who_can_access_room',
43+
value: false,
44+
},
45+
],
46+
});
47+
48+
await this.add('FileUpload_Restrict_to_users_who_can_access_room', false, {
49+
type: 'boolean',
50+
enableQuery: [
51+
{
52+
_id: 'FileUpload_ProtectFiles',
53+
value: true,
54+
},
55+
{
56+
_id: 'FileUpload_Restrict_to_room_members',
57+
value: false,
58+
},
59+
],
4060
});
4161

4262
await this.add('FileUpload_RotateImages', true, {

apps/meteor/tests/end-to-end/api/09-rooms.js

+34
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,13 @@ describe('[Rooms]', function () {
8787
let userCredentials;
8888
const testChannelName = `channel.test.upload.${Date.now()}-${Math.random()}`;
8989
let blockedMediaTypes;
90+
let testPrivateChannel;
9091

9192
before(async () => {
9293
user = await createUser({ joinDefaultChannels: false });
9394
userCredentials = await login(user.username, password);
9495
testChannel = (await createRoom({ type: 'c', name: testChannelName })).body.channel;
96+
testPrivateChannel = (await createRoom({ type: 'p', name: `channel.test.private.${Date.now()}-${Math.random()}` })).body.group;
9597
blockedMediaTypes = await getSettingValueById('FileUpload_MediaTypeBlackList');
9698
const newBlockedMediaTypes = blockedMediaTypes
9799
.split(',')
@@ -105,8 +107,10 @@ describe('[Rooms]', function () {
105107
deleteRoom({ type: 'c', roomId: testChannel._id }),
106108
deleteUser(user),
107109
updateSetting('FileUpload_Restrict_to_room_members', true),
110+
updateSetting('FileUpload_Restrict_to_users_who_can_access_room', false),
108111
updateSetting('FileUpload_ProtectFiles', true),
109112
updateSetting('FileUpload_MediaTypeBlackList', blockedMediaTypes),
113+
deleteRoom({ roomId: testPrivateChannel._id, type: 'p' }),
110114
]),
111115
);
112116

@@ -221,6 +225,7 @@ describe('[Rooms]', function () {
221225

222226
it('should be able to get the file when no access to the room if setting allows it', async () => {
223227
await updateSetting('FileUpload_Restrict_to_room_members', false);
228+
await updateSetting('FileUpload_Restrict_to_users_who_can_access_room', false);
224229
await request.get(fileNewUrl).set(userCredentials).expect('Content-Type', 'image/png').expect(200);
225230
await request.get(fileOldUrl).set(userCredentials).expect('Content-Type', 'image/png').expect(200);
226231
});
@@ -237,6 +242,35 @@ describe('[Rooms]', function () {
237242
await request.get(fileOldUrl).set(credentials).expect('Content-Type', 'image/png').expect(200);
238243
});
239244

245+
it('should be able to get the file if not member but can access room if setting allows', async () => {
246+
await updateSetting('FileUpload_Restrict_to_room_members', false);
247+
await updateSetting('FileUpload_Restrict_to_users_who_can_access_room', true);
248+
249+
await request.get(fileNewUrl).set(userCredentials).expect('Content-Type', 'image/png').expect(200);
250+
await request.get(fileOldUrl).set(userCredentials).expect('Content-Type', 'image/png').expect(200);
251+
});
252+
253+
it('should not be able to get the file if not member and cannot access room', async () => {
254+
const { body } = await request
255+
.post(api(`rooms.upload/${testPrivateChannel._id}`))
256+
.set(credentials)
257+
.attach('file', imgURL)
258+
.expect('Content-Type', 'application/json')
259+
.expect(200);
260+
261+
const fileUrl = `/file-upload/${body.message.file._id}/${body.message.file.name}`;
262+
263+
await request.get(fileUrl).set(userCredentials).expect(403);
264+
});
265+
266+
it('should respect the setting with less permissions when both are true', async () => {
267+
await updateSetting('FileUpload_ProtectFiles', true);
268+
await updateSetting('FileUpload_Restrict_to_room_members', true);
269+
await updateSetting('FileUpload_Restrict_to_users_who_can_access_room', true);
270+
await request.get(fileNewUrl).set(userCredentials).expect(403);
271+
await request.get(fileOldUrl).set(userCredentials).expect(403);
272+
});
273+
240274
it('should not be able to get the file without credentials', async () => {
241275
await request.get(fileNewUrl).attach('file', imgURL).expect(403);
242276
await request.get(fileOldUrl).attach('file', imgURL).expect(403);

ee/packages/omnichannel-services/src/OmnichannelTranscript.ts

+54-22
Original file line numberDiff line numberDiff line change
@@ -336,22 +336,15 @@ export class OmnichannelTranscript extends ServiceClass implements IOmnichannelT
336336
const outBuff = await streamToBuffer(stream as Readable);
337337

338338
try {
339-
const file = await uploadService.uploadFile({
340-
userId: details.userId,
339+
const { rid } = await roomService.createDirectMessage({ to: details.userId, from: 'rocket.cat' });
340+
const [rocketCatFile, transcriptFile] = await this.uploadFiles({
341+
details,
341342
buffer: outBuff,
342-
details: {
343-
// transcript_{company-name)_{date}_{hour}.pdf
344-
name: `${transcriptText}_${data.siteName}_${new Intl.DateTimeFormat('en-US').format(new Date())}_${
345-
data.visitor?.name || data.visitor?.username || 'Visitor'
346-
}.pdf`,
347-
type: 'application/pdf',
348-
rid: details.rid,
349-
// Rocket.cat is the goat
350-
userId: 'rocket.cat',
351-
size: outBuff.length,
352-
},
343+
roomIds: [rid, details.rid],
344+
data,
345+
transcriptText,
353346
});
354-
await this.pdfComplete({ details, file });
347+
await this.pdfComplete({ details, transcriptFile, rocketCatFile });
355348
} catch (e: any) {
356349
this.pdfFailed({ details, e });
357350
}
@@ -380,35 +373,74 @@ export class OmnichannelTranscript extends ServiceClass implements IOmnichannelT
380373
});
381374
}
382375

383-
private async pdfComplete({ details, file }: { details: WorkDetailsWithSource; file: IUpload }): Promise<void> {
376+
private async uploadFiles({
377+
details,
378+
buffer,
379+
roomIds,
380+
data,
381+
transcriptText,
382+
}: {
383+
details: WorkDetailsWithSource;
384+
buffer: Buffer;
385+
roomIds: string[];
386+
data: any;
387+
transcriptText: string;
388+
}): Promise<IUpload[]> {
389+
return Promise.all(
390+
roomIds.map((roomId) => {
391+
return uploadService.uploadFile({
392+
userId: details.userId,
393+
buffer,
394+
details: {
395+
// transcript_{company-name}_{date}_{hour}.pdf
396+
name: `${transcriptText}_${data.siteName}_${new Intl.DateTimeFormat('en-US').format(new Date())}_${
397+
data.visitor?.name || data.visitor?.username || 'Visitor'
398+
}.pdf`,
399+
type: 'application/pdf',
400+
rid: roomId,
401+
// Rocket.cat is the goat
402+
userId: 'rocket.cat',
403+
size: buffer.length,
404+
},
405+
});
406+
}),
407+
);
408+
}
409+
410+
private async pdfComplete({
411+
details,
412+
transcriptFile,
413+
rocketCatFile,
414+
}: {
415+
details: WorkDetailsWithSource;
416+
transcriptFile: IUpload;
417+
rocketCatFile: IUpload;
418+
}): Promise<void> {
384419
this.log.info(`Transcript for room ${details.rid} by user ${details.userId} - Complete`);
385420
const user = await Users.findOneById(details.userId);
386421
if (!user) {
387422
return;
388423
}
389424
// Send the file to the livechat room where this was requested, to keep it in context
390425
try {
391-
const [, { rid }] = await Promise.all([
392-
LivechatRooms.setPdfTranscriptFileIdById(details.rid, file._id),
393-
roomService.createDirectMessage({ to: details.userId, from: 'rocket.cat' }),
394-
]);
426+
await LivechatRooms.setPdfTranscriptFileIdById(details.rid, transcriptFile._id);
395427

396428
this.log.info(`Transcript for room ${details.rid} by user ${details.userId} - Sending success message to user`);
397429
const result = await Promise.allSettled([
398430
uploadService.sendFileMessage({
399431
roomId: details.rid,
400432
userId: 'rocket.cat',
401-
file,
433+
file: transcriptFile,
402434
message: {
403435
// Translate from service
404436
msg: await translationService.translateToServerLanguage('pdf_success_message'),
405437
},
406438
}),
407439
// Send the file to the user who requested it, so they can download it
408440
uploadService.sendFileMessage({
409-
roomId: rid,
441+
roomId: rocketCatFile.rid || '',
410442
userId: 'rocket.cat',
411-
file,
443+
file: rocketCatFile,
412444
message: {
413445
// Translate from service
414446
msg: await translationService.translate('pdf_success_message', user),

packages/i18n/src/locales/en.i18n.json

+2
Original file line numberDiff line numberDiff line change
@@ -2354,6 +2354,8 @@
23542354
"FileUpload_Enable_json_web_token_for_files_description": "Appends a JWT to uploaded files urls",
23552355
"FileUpload_Restrict_to_room_members": "Restrict files to rooms' members",
23562356
"FileUpload_Restrict_to_room_members_Description": "Restrict the access of files uploaded on rooms to the rooms' members only",
2357+
"FileUpload_Restrict_to_users_who_can_access_room": "Restrict files to users who can access the room",
2358+
"FileUpload_Restrict_to_users_who_can_access_room_Description": "Restrict the access of files uploaded on rooms to the users who can access the room. This option is mutually exclusive with the \"Restrict files to rooms' members\" option as this one allows for users that are not part of some rooms but have special permissions that allow them to see it to access the files uploaded, for example, Omnichannel Managers & Monitors",
23572359
"FileUpload_Enabled": "File Uploads Enabled",
23582360
"FileUpload_Enabled_Direct": "File Uploads Enabled in Direct Messages ",
23592361
"FileUpload_Error": "File Upload Error",

0 commit comments

Comments
 (0)