@stamhoofd/backend 2.79.1 → 2.79.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +10 -10
- package/src/decoders/StringArrayDecoder.ts +3 -0
- package/src/endpoints/auth/CreateAdminEndpoint.ts +1 -1
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +4 -4
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +1 -1
- package/src/endpoints/global/webshops/GetWebshopFromDomainEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +20 -14
- package/src/helpers/AdminPermissionChecker.ts +8 -6
- package/src/services/FileSignService.ts +20 -9
- package/tests/e2e/private-files.test.ts +176 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.79.
|
|
3
|
+
"version": "2.79.3",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -38,14 +38,14 @@
|
|
|
38
38
|
"@simonbackx/simple-encoding": "2.21.0",
|
|
39
39
|
"@simonbackx/simple-endpoints": "1.19.1",
|
|
40
40
|
"@simonbackx/simple-logging": "^1.0.1",
|
|
41
|
-
"@stamhoofd/backend-i18n": "2.79.
|
|
42
|
-
"@stamhoofd/backend-middleware": "2.79.
|
|
43
|
-
"@stamhoofd/email": "2.79.
|
|
44
|
-
"@stamhoofd/models": "2.79.
|
|
45
|
-
"@stamhoofd/queues": "2.79.
|
|
46
|
-
"@stamhoofd/sql": "2.79.
|
|
47
|
-
"@stamhoofd/structures": "2.79.
|
|
48
|
-
"@stamhoofd/utility": "2.79.
|
|
41
|
+
"@stamhoofd/backend-i18n": "2.79.3",
|
|
42
|
+
"@stamhoofd/backend-middleware": "2.79.3",
|
|
43
|
+
"@stamhoofd/email": "2.79.3",
|
|
44
|
+
"@stamhoofd/models": "2.79.3",
|
|
45
|
+
"@stamhoofd/queues": "2.79.3",
|
|
46
|
+
"@stamhoofd/sql": "2.79.3",
|
|
47
|
+
"@stamhoofd/structures": "2.79.3",
|
|
48
|
+
"@stamhoofd/utility": "2.79.3",
|
|
49
49
|
"archiver": "^7.0.1",
|
|
50
50
|
"aws-sdk": "^2.885.0",
|
|
51
51
|
"axios": "1.6.8",
|
|
@@ -65,5 +65,5 @@
|
|
|
65
65
|
"publishConfig": {
|
|
66
66
|
"access": "public"
|
|
67
67
|
},
|
|
68
|
-
"gitHead": "
|
|
68
|
+
"gitHead": "3b9878a9052295f3c5f9a661fbb8219ed87b23fe"
|
|
69
69
|
}
|
|
@@ -101,7 +101,7 @@ export class CreateAdminEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
101
101
|
const platformName = ((await Platform.getSharedStruct()).config.name);
|
|
102
102
|
|
|
103
103
|
const name = organization?.name ?? platformName;
|
|
104
|
-
const what = organization ? $t('
|
|
104
|
+
const what = organization ? $t('a5c0dce2-01df-4a57-8d02-0b79cec9b89d', { name, platform: platformName }) : platformName;
|
|
105
105
|
|
|
106
106
|
const emailTo = admin.getEmailTo();
|
|
107
107
|
const email: string = typeof emailTo === 'string' ? emailTo : emailTo[0]?.email;
|
|
@@ -1027,11 +1027,11 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
1027
1027
|
message: 'Maximum members reached',
|
|
1028
1028
|
human: responsibility.maximumMembers === 1
|
|
1029
1029
|
? (model.groupId
|
|
1030
|
-
? $t('
|
|
1031
|
-
: $t('
|
|
1030
|
+
? $t('e3e4ba16-7923-42bc-ae23-cd729ce06869', { responsibility: responsibility.name })
|
|
1031
|
+
: $t('77e408e8-59e5-42c2-b58d-956f7c391e5c', { responsibility: responsibility.name }))
|
|
1032
1032
|
: (model.groupId
|
|
1033
|
-
? $t('
|
|
1034
|
-
: $t('
|
|
1033
|
+
? $t('10c13841-9f58-4651-a9b3-a34c8ce1a505', { count: responsibility.maximumMembers.toFixed(), responsibility: responsibility.name })
|
|
1034
|
+
: $t('01ef9768-89b5-48ea-955e-b896306a9a87', { count: responsibility.maximumMembers.toFixed(), responsibility: responsibility.name })),
|
|
1035
1035
|
});
|
|
1036
1036
|
}
|
|
1037
1037
|
}
|
|
@@ -247,7 +247,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
247
247
|
throw new SimpleError({
|
|
248
248
|
code: 'forbidden',
|
|
249
249
|
message: 'No permission to register in this group',
|
|
250
|
-
human: $t('
|
|
250
|
+
human: $t('36e8f895-91df-4c88-88e7-d4f0e9d1b5bf', { group: group.settings.name }),
|
|
251
251
|
statusCode: 403,
|
|
252
252
|
});
|
|
253
253
|
}
|
|
@@ -11,8 +11,8 @@ class Query extends AutoEncoder {
|
|
|
11
11
|
@field({ decoder: StringDecoder })
|
|
12
12
|
domain: string;
|
|
13
13
|
|
|
14
|
-
@field({ decoder: StringDecoder,
|
|
15
|
-
uri
|
|
14
|
+
@field({ decoder: StringDecoder, optional: true })
|
|
15
|
+
uri = '';
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
type Body = undefined;
|
|
@@ -430,6 +430,9 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
430
430
|
patch.waitingList,
|
|
431
431
|
model.organizationId,
|
|
432
432
|
requiredPeriod,
|
|
433
|
+
{
|
|
434
|
+
allowedIds: [patch.waitingList.id],
|
|
435
|
+
},
|
|
433
436
|
);
|
|
434
437
|
model.waitingListId = group.id;
|
|
435
438
|
}
|
|
@@ -443,12 +446,14 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
443
446
|
static async createGroup(struct: GroupStruct, organizationId: string, period: RegistrationPeriod, options?: { allowedIds?: string[] }): Promise<Group> {
|
|
444
447
|
const allowedIds = options?.allowedIds ?? [];
|
|
445
448
|
|
|
446
|
-
if (
|
|
447
|
-
if (
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
449
|
+
if (struct.type === GroupType.Membership || struct.type === GroupType.WaitingList) {
|
|
450
|
+
if (!await Context.auth.hasFullAccess(organizationId)) {
|
|
451
|
+
if (allowedIds.includes(struct.id)) {
|
|
452
|
+
// Ok
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
throw Context.auth.error('Je hebt geen toegangsrechten om groepen toe te voegen');
|
|
456
|
+
}
|
|
452
457
|
}
|
|
453
458
|
}
|
|
454
459
|
|
|
@@ -493,15 +498,15 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
493
498
|
user.permissions!.organizationPermissions.set(organizationId, organizationPermissions.patch(patch));
|
|
494
499
|
console.log('Automatically granted author full permissions to resource', 'group', model.id, 'user', user.id, 'patch', patch.encode({ version: Version }));
|
|
495
500
|
await user.save();
|
|
496
|
-
}
|
|
497
501
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
502
|
+
// Check if current user has permissions to this new group -> else fail with error
|
|
503
|
+
if (!await Context.auth.canAccessGroup(model, PermissionLevel.Full)) {
|
|
504
|
+
throw new SimpleError({
|
|
505
|
+
code: 'missing_permissions',
|
|
506
|
+
message: 'You cannot restrict your own permissions',
|
|
507
|
+
human: 'Je kan geen inschrijvingsgroep maken zonder dat je zelf volledige toegang hebt tot de nieuwe groep',
|
|
508
|
+
});
|
|
509
|
+
}
|
|
505
510
|
}
|
|
506
511
|
|
|
507
512
|
if (struct.waitingList) {
|
|
@@ -523,6 +528,7 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
523
528
|
struct.waitingList,
|
|
524
529
|
model.organizationId,
|
|
525
530
|
period,
|
|
531
|
+
{ allowedIds: [struct.waitingList.id] },
|
|
526
532
|
);
|
|
527
533
|
model.waitingListId = group.id;
|
|
528
534
|
}
|
|
@@ -92,7 +92,7 @@ export class AdminPermissionChecker {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
memberNotFoundOrNoAccess(): SimpleError {
|
|
95
|
-
return this.notFoundOrNoAccess($t('
|
|
95
|
+
return this.notFoundOrNoAccess($t('d24814a3-aedc-4569-9ab3-f854027c4e9f'));
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
notFoundOrNoAccess(message?: string): SimpleError {
|
|
@@ -195,11 +195,13 @@ export class AdminPermissionChecker {
|
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
// Check parent categories
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
198
|
+
if (group.type === GroupType.Membership) {
|
|
199
|
+
const organizationPeriod = await this.getOrganizationCurrentPeriod(organization);
|
|
200
|
+
const parentCategories = group.getParentCategories(organizationPeriod.settings.categories);
|
|
201
|
+
for (const category of parentCategories) {
|
|
202
|
+
if (organizationPermissions.hasResourceAccess(PermissionsResourceType.GroupCategories, category.id, permissionLevel)) {
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
203
205
|
}
|
|
204
206
|
}
|
|
205
207
|
|
|
@@ -70,7 +70,9 @@ export class FileSignService {
|
|
|
70
70
|
if (e instanceof jose.errors.JWSSignatureVerificationFailed) {
|
|
71
71
|
return false;
|
|
72
72
|
}
|
|
73
|
-
|
|
73
|
+
if (STAMHOOFD.environment !== 'test') {
|
|
74
|
+
console.error('Failed to verify file signature:', e);
|
|
75
|
+
}
|
|
74
76
|
return false;
|
|
75
77
|
}
|
|
76
78
|
};
|
|
@@ -120,15 +122,19 @@ export class FileSignService {
|
|
|
120
122
|
}
|
|
121
123
|
}
|
|
122
124
|
|
|
123
|
-
static async fillSignedUrlsForStruct(data: any) {
|
|
125
|
+
static async fillSignedUrlsForStruct(data: any, looped = new Set()) {
|
|
124
126
|
if (data instanceof File) {
|
|
125
127
|
return (await data.withSignedUrl()) ?? undefined; // never return null if it fails because we'll want to use the original file in that case
|
|
126
128
|
}
|
|
129
|
+
if (looped.has(data)) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
looped.add(data);
|
|
127
133
|
|
|
128
134
|
if (Array.isArray(data)) {
|
|
129
135
|
for (let i = 0; i < data.length; i++) {
|
|
130
136
|
const value = data[i];
|
|
131
|
-
const r = await this.fillSignedUrlsForStruct(value);
|
|
137
|
+
const r = await this.fillSignedUrlsForStruct(value, looped);
|
|
132
138
|
if (r !== undefined) {
|
|
133
139
|
data[i] = r;
|
|
134
140
|
}
|
|
@@ -138,7 +144,7 @@ export class FileSignService {
|
|
|
138
144
|
|
|
139
145
|
if (data instanceof Map) {
|
|
140
146
|
for (const [key, value] of data.entries()) {
|
|
141
|
-
const r = await this.fillSignedUrlsForStruct(value);
|
|
147
|
+
const r = await this.fillSignedUrlsForStruct(value, looped);
|
|
142
148
|
|
|
143
149
|
if (r !== undefined) {
|
|
144
150
|
data.set(key, r);
|
|
@@ -151,7 +157,7 @@ export class FileSignService {
|
|
|
151
157
|
// Loop all keys and search for File objects + replace them with the signed variant
|
|
152
158
|
if (typeof data === 'object' && data !== null) {
|
|
153
159
|
for (const key in data) {
|
|
154
|
-
const r = await this.fillSignedUrlsForStruct(data[key]);
|
|
160
|
+
const r = await this.fillSignedUrlsForStruct(data[key], looped);
|
|
155
161
|
if (r !== undefined) {
|
|
156
162
|
data[key] = r;
|
|
157
163
|
}
|
|
@@ -160,7 +166,12 @@ export class FileSignService {
|
|
|
160
166
|
}
|
|
161
167
|
}
|
|
162
168
|
|
|
163
|
-
static async verifyFilesInStruct(data: any) {
|
|
169
|
+
static async verifyFilesInStruct(data: any, looped = new Set()) {
|
|
170
|
+
if (looped.has(data)) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
looped.add(data);
|
|
174
|
+
|
|
164
175
|
if (data instanceof File) {
|
|
165
176
|
if (!data.isPrivate) {
|
|
166
177
|
return;
|
|
@@ -183,14 +194,14 @@ export class FileSignService {
|
|
|
183
194
|
|
|
184
195
|
if (Array.isArray(data)) {
|
|
185
196
|
for (const value of data) {
|
|
186
|
-
await this.verifyFilesInStruct(value);
|
|
197
|
+
await this.verifyFilesInStruct(value, looped);
|
|
187
198
|
}
|
|
188
199
|
return;
|
|
189
200
|
}
|
|
190
201
|
|
|
191
202
|
if (data instanceof Map) {
|
|
192
203
|
for (const [key, value] of data.entries()) {
|
|
193
|
-
await this.verifyFilesInStruct(value);
|
|
204
|
+
await this.verifyFilesInStruct(value, looped);
|
|
194
205
|
}
|
|
195
206
|
return;
|
|
196
207
|
}
|
|
@@ -198,7 +209,7 @@ export class FileSignService {
|
|
|
198
209
|
// Loop all keys and search for File objects + replace them with the signed variant
|
|
199
210
|
if (typeof data === 'object' && data !== null) {
|
|
200
211
|
for (const key in data) {
|
|
201
|
-
await this.verifyFilesInStruct(data[key]);
|
|
212
|
+
await this.verifyFilesInStruct(data[key], looped);
|
|
202
213
|
}
|
|
203
214
|
return;
|
|
204
215
|
}
|
|
@@ -7,6 +7,7 @@ import { PermissionLevel, Permissions } from '@stamhoofd/structures';
|
|
|
7
7
|
import { PatchUserMembersEndpoint } from '../../src/endpoints/global/registration/PatchUserMembersEndpoint';
|
|
8
8
|
import { testServer } from '../helpers/TestServer';
|
|
9
9
|
import { GetUserMembersEndpoint } from '../../src/endpoints/global/registration/GetUserMembersEndpoint';
|
|
10
|
+
import { FileSignService } from '../../src/services/FileSignService';
|
|
10
11
|
|
|
11
12
|
const baseUrl = `/v${Version}/members`;
|
|
12
13
|
const endpoint = new PatchUserMembersEndpoint();
|
|
@@ -413,6 +414,181 @@ describe('E2E.PrivateFiles', () => {
|
|
|
413
414
|
expect(answer.file!.signedUrl).not.toEqual('https://test.com/test.exe'); // It got replaced with a proper signed url
|
|
414
415
|
});
|
|
415
416
|
|
|
417
|
+
describe('fillSignedUrlsForStruct', () => {
|
|
418
|
+
test('Can handle circular references', async () => {
|
|
419
|
+
// A malicious user could try to set a private file to a member in order to get
|
|
420
|
+
// access to the signed URL. This should not be possible without a valid signature
|
|
421
|
+
const privateFile = new File({
|
|
422
|
+
id: 'test',
|
|
423
|
+
server: 'test.com',
|
|
424
|
+
path: 'test.txt',
|
|
425
|
+
size: 100,
|
|
426
|
+
isPrivate: true,
|
|
427
|
+
});
|
|
428
|
+
await privateFile.sign();
|
|
429
|
+
|
|
430
|
+
const data = {
|
|
431
|
+
hello: 'true',
|
|
432
|
+
world: 'false',
|
|
433
|
+
circular: {
|
|
434
|
+
data: {
|
|
435
|
+
here: null as any,
|
|
436
|
+
},
|
|
437
|
+
file: privateFile,
|
|
438
|
+
},
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
data.circular.data.here = data;
|
|
442
|
+
|
|
443
|
+
await FileSignService.fillSignedUrlsForStruct(data);
|
|
444
|
+
expect(data.circular.file.signedUrl).toBeString();
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test('Can handle duplicate files', async () => {
|
|
448
|
+
// A malicious user could try to set a private file to a member in order to get
|
|
449
|
+
// access to the signed URL. This should not be possible without a valid signature
|
|
450
|
+
const privateFile = new File({
|
|
451
|
+
id: 'test',
|
|
452
|
+
server: 'test.com',
|
|
453
|
+
path: 'test.txt',
|
|
454
|
+
size: 100,
|
|
455
|
+
isPrivate: true,
|
|
456
|
+
});
|
|
457
|
+
await privateFile.sign();
|
|
458
|
+
|
|
459
|
+
const data = {
|
|
460
|
+
hello: 'true',
|
|
461
|
+
world: 'false',
|
|
462
|
+
circular: {
|
|
463
|
+
file1: privateFile,
|
|
464
|
+
file2: privateFile,
|
|
465
|
+
},
|
|
466
|
+
arr: [
|
|
467
|
+
privateFile,
|
|
468
|
+
privateFile,
|
|
469
|
+
],
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
await FileSignService.fillSignedUrlsForStruct(data);
|
|
473
|
+
expect(data.circular.file1.signedUrl).toBeString();
|
|
474
|
+
expect(data.circular.file2.signedUrl).toBeString();
|
|
475
|
+
expect(data.arr[0].signedUrl).toBeString();
|
|
476
|
+
expect(data.arr[1].signedUrl).toBeString();
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
describe('verifyFilesInStruct', () => {
|
|
481
|
+
test('Can handle circular references that are properly signed', async () => {
|
|
482
|
+
// A malicious user could try to set a private file to a member in order to get
|
|
483
|
+
// access to the signed URL. This should not be possible without a valid signature
|
|
484
|
+
const privateFile = new File({
|
|
485
|
+
id: 'test',
|
|
486
|
+
server: 'test.com',
|
|
487
|
+
path: 'test.txt',
|
|
488
|
+
size: 100,
|
|
489
|
+
isPrivate: true,
|
|
490
|
+
});
|
|
491
|
+
await privateFile.sign();
|
|
492
|
+
|
|
493
|
+
const data = {
|
|
494
|
+
hello: 'true',
|
|
495
|
+
world: 'false',
|
|
496
|
+
circular: {
|
|
497
|
+
data: {
|
|
498
|
+
here: null as any,
|
|
499
|
+
},
|
|
500
|
+
file: privateFile,
|
|
501
|
+
},
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
data.circular.data.here = data;
|
|
505
|
+
|
|
506
|
+
await expect(FileSignService.verifyFilesInStruct(data)).toResolve();
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
test('Can handle duplicate files that are properly signed', async () => {
|
|
510
|
+
// A malicious user could try to set a private file to a member in order to get
|
|
511
|
+
// access to the signed URL. This should not be possible without a valid signature
|
|
512
|
+
const privateFile = new File({
|
|
513
|
+
id: 'test',
|
|
514
|
+
server: 'test.com',
|
|
515
|
+
path: 'test.txt',
|
|
516
|
+
size: 100,
|
|
517
|
+
isPrivate: true,
|
|
518
|
+
});
|
|
519
|
+
await privateFile.sign();
|
|
520
|
+
|
|
521
|
+
const data = {
|
|
522
|
+
hello: 'true',
|
|
523
|
+
world: 'false',
|
|
524
|
+
circular: {
|
|
525
|
+
file1: privateFile,
|
|
526
|
+
file2: privateFile,
|
|
527
|
+
},
|
|
528
|
+
arr: [
|
|
529
|
+
privateFile,
|
|
530
|
+
privateFile,
|
|
531
|
+
],
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
await expect(FileSignService.verifyFilesInStruct(data)).toResolve();
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
test('Can handle circular references with files that are not signed', async () => {
|
|
538
|
+
// A malicious user could try to set a private file to a member in order to get
|
|
539
|
+
// access to the signed URL. This should not be possible without a valid signature
|
|
540
|
+
const privateFile = new File({
|
|
541
|
+
id: 'test',
|
|
542
|
+
server: 'test.com',
|
|
543
|
+
path: 'test.txt',
|
|
544
|
+
size: 100,
|
|
545
|
+
isPrivate: true,
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
const data = {
|
|
549
|
+
hello: 'true',
|
|
550
|
+
world: 'false',
|
|
551
|
+
circular: {
|
|
552
|
+
data: {
|
|
553
|
+
here: null as any,
|
|
554
|
+
},
|
|
555
|
+
file: privateFile,
|
|
556
|
+
},
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
data.circular.data.here = data;
|
|
560
|
+
|
|
561
|
+
await expect(FileSignService.verifyFilesInStruct(data)).rejects.toThrow(/Invalid signature for file/);
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
test('Can handle duplicate files that are not signed', async () => {
|
|
565
|
+
// A malicious user could try to set a private file to a member in order to get
|
|
566
|
+
// access to the signed URL. This should not be possible without a valid signature
|
|
567
|
+
const privateFile = new File({
|
|
568
|
+
id: 'test',
|
|
569
|
+
server: 'test.com',
|
|
570
|
+
path: 'test.txt',
|
|
571
|
+
size: 100,
|
|
572
|
+
isPrivate: true,
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
const data = {
|
|
576
|
+
hello: 'true',
|
|
577
|
+
world: 'false',
|
|
578
|
+
circular: {
|
|
579
|
+
file1: privateFile,
|
|
580
|
+
file2: privateFile,
|
|
581
|
+
},
|
|
582
|
+
arr: [
|
|
583
|
+
privateFile,
|
|
584
|
+
privateFile,
|
|
585
|
+
],
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
await expect(FileSignService.verifyFilesInStruct(data)).rejects.toThrow(/Invalid signature for file/);
|
|
589
|
+
});
|
|
590
|
+
});
|
|
591
|
+
|
|
416
592
|
/**
|
|
417
593
|
* Tests that when an unverified file is stored on the server with a signed url, the server will never return that signed url.
|
|
418
594
|
*/
|