@stamhoofd/backend 2.74.0 → 2.75.1
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/index.ts +7 -2
- package/package.json +13 -13
- package/src/crons/update-cached-balances.ts +1 -2
- package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +2 -2
- package/src/endpoints/auth/CreateAdminEndpoint.ts +4 -15
- package/src/endpoints/auth/OpenIDConnectStartEndpoint.ts +0 -5
- package/src/endpoints/global/audit-logs/GetAuditLogsEndpoint.ts +2 -2
- package/src/endpoints/global/events/GetEventNotificationsCountEndpoint.ts +43 -0
- package/src/endpoints/global/events/GetEventNotificationsEndpoint.ts +181 -0
- package/src/endpoints/global/events/GetEventsEndpoint.ts +2 -2
- package/src/endpoints/global/events/PatchEventNotificationsEndpoint.ts +288 -0
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +2 -2
- package/src/endpoints/global/files/UploadFile.ts +56 -4
- package/src/endpoints/global/files/UploadImage.ts +9 -3
- package/src/endpoints/global/members/GetMembersEndpoint.ts +2 -2
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +10 -1
- package/src/endpoints/global/platform/GetPlatformAdminsEndpoint.ts +1 -5
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +7 -0
- package/src/endpoints/global/registration/GetUserDocumentsEndpoint.ts +1 -1
- package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +2084 -164
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +2 -2
- package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +48 -2
- package/src/endpoints/organization/dashboard/documents/GetDocumentsEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +8 -0
- package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.ts +3 -3
- package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/webshops/GetWebshopTicketsEndpoint.ts +2 -2
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +1 -2
- package/src/helpers/AdminPermissionChecker.ts +80 -2
- package/src/helpers/AuthenticatedStructures.ts +88 -2
- package/src/helpers/FlagMomentCleanup.ts +1 -8
- package/src/helpers/GlobalHelper.ts +15 -0
- package/src/helpers/MembershipCharger.ts +2 -1
- package/src/services/EventNotificationService.ts +201 -0
- package/src/services/FileSignService.ts +217 -0
- package/src/services/SSOService.ts +7 -2
- package/src/sql-filters/event-notifications.ts +39 -0
- package/src/sql-filters/organizations.ts +1 -1
- package/src/sql-sorters/event-notifications.ts +96 -0
- package/src/sql-sorters/events.ts +2 -2
- package/src/sql-sorters/organizations.ts +2 -2
- package/tests/e2e/private-files.test.ts +497 -0
- package/tests/e2e/register.test.ts +1197 -0
- package/tests/helpers/TestServer.ts +3 -0
- package/tests/jest.setup.ts +15 -2
- package/tsconfig.json +1 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
import { File, MemberDetails, MemberWithRegistrationsBlob, RecordCategory, RecordFileAnswer, RecordSettings, RecordType, Version } from '@stamhoofd/structures';
|
|
2
|
+
|
|
3
|
+
import { PatchableArray, PatchableArrayAutoEncoder } from '@simonbackx/simple-encoding';
|
|
4
|
+
import { Request } from '@simonbackx/simple-endpoints';
|
|
5
|
+
import { Member, OrganizationFactory, Token, UserFactory } from '@stamhoofd/models';
|
|
6
|
+
import { PermissionLevel, Permissions } from '@stamhoofd/structures';
|
|
7
|
+
import { PatchUserMembersEndpoint } from '../../src/endpoints/global/registration/PatchUserMembersEndpoint';
|
|
8
|
+
import { testServer } from '../helpers/TestServer';
|
|
9
|
+
import { GetUserMembersEndpoint } from '../../src/endpoints/global/registration/GetUserMembersEndpoint';
|
|
10
|
+
|
|
11
|
+
const baseUrl = `/v${Version}/members`;
|
|
12
|
+
const endpoint = new PatchUserMembersEndpoint();
|
|
13
|
+
const getMembersEndpoint = new GetUserMembersEndpoint();
|
|
14
|
+
|
|
15
|
+
describe('E2E.PrivateFiles', () => {
|
|
16
|
+
const recordSettings = RecordSettings.create({
|
|
17
|
+
id: 'test',
|
|
18
|
+
name: 'Bestand test',
|
|
19
|
+
type: RecordType.File,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
async function createOrganization() {
|
|
23
|
+
const organization = await new OrganizationFactory({}).create();
|
|
24
|
+
|
|
25
|
+
// Add record settings type
|
|
26
|
+
const category = RecordCategory.create({
|
|
27
|
+
name: 'Voorbeeld',
|
|
28
|
+
defaultEnabled: true,
|
|
29
|
+
records: [
|
|
30
|
+
recordSettings,
|
|
31
|
+
],
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
organization.meta.recordsConfiguration.recordCategories = [category];
|
|
35
|
+
await organization.save();
|
|
36
|
+
|
|
37
|
+
return organization;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
test('Cannot set unsigned private files', async () => {
|
|
41
|
+
const organization = await createOrganization();
|
|
42
|
+
|
|
43
|
+
const user = await new UserFactory({
|
|
44
|
+
organization,
|
|
45
|
+
permissions: Permissions.create({
|
|
46
|
+
level: PermissionLevel.Full,
|
|
47
|
+
}),
|
|
48
|
+
}).create();
|
|
49
|
+
const token = await Token.createToken(user);
|
|
50
|
+
|
|
51
|
+
// Create a user member
|
|
52
|
+
const member = MemberWithRegistrationsBlob.create({
|
|
53
|
+
details: MemberDetails.create({
|
|
54
|
+
firstName: 'John',
|
|
55
|
+
lastName: 'Doe',
|
|
56
|
+
}),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// A malicious user could try to set a private file to a member in order to get
|
|
60
|
+
// access to the signed URL. This should not be possible without a valid signature
|
|
61
|
+
const privateFile = new File({
|
|
62
|
+
id: 'test',
|
|
63
|
+
server: 'test.com',
|
|
64
|
+
path: 'test.txt',
|
|
65
|
+
size: 100,
|
|
66
|
+
isPrivate: true,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
member.details.recordAnswers.set(recordSettings.id, RecordFileAnswer.create({
|
|
70
|
+
file: privateFile,
|
|
71
|
+
settings: recordSettings,
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
const arr = new PatchableArray() as PatchableArrayAutoEncoder<MemberWithRegistrationsBlob>;
|
|
75
|
+
arr.addPut(member);
|
|
76
|
+
|
|
77
|
+
const r = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
|
|
78
|
+
r.headers.authorization = 'Bearer ' + token.accessToken;
|
|
79
|
+
|
|
80
|
+
await expect(testServer.test(endpoint, r)).rejects.toThrow(/Missing signature for private file/);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('Cannot set private files with invalid signatures', async () => {
|
|
84
|
+
const organization = await createOrganization();
|
|
85
|
+
|
|
86
|
+
const user = await new UserFactory({
|
|
87
|
+
organization,
|
|
88
|
+
permissions: Permissions.create({
|
|
89
|
+
level: PermissionLevel.Full,
|
|
90
|
+
}),
|
|
91
|
+
}).create();
|
|
92
|
+
const token = await Token.createToken(user);
|
|
93
|
+
|
|
94
|
+
// Create a user member
|
|
95
|
+
const member = MemberWithRegistrationsBlob.create({
|
|
96
|
+
details: MemberDetails.create({
|
|
97
|
+
firstName: 'John',
|
|
98
|
+
lastName: 'Doe',
|
|
99
|
+
}),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// A malicious user could try to set a private file to a member in order to get
|
|
103
|
+
// access to the signed URL. This should not be possible without a valid signature
|
|
104
|
+
const privateFile = new File({
|
|
105
|
+
id: 'test',
|
|
106
|
+
server: 'test.com',
|
|
107
|
+
path: 'test.txt',
|
|
108
|
+
size: 100,
|
|
109
|
+
isPrivate: true,
|
|
110
|
+
signature: 'invalid',
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
member.details.recordAnswers.set(recordSettings.id, RecordFileAnswer.create({
|
|
114
|
+
file: privateFile,
|
|
115
|
+
settings: recordSettings,
|
|
116
|
+
}));
|
|
117
|
+
|
|
118
|
+
const arr = new PatchableArray() as PatchableArrayAutoEncoder<MemberWithRegistrationsBlob>;
|
|
119
|
+
arr.addPut(member);
|
|
120
|
+
|
|
121
|
+
const r = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
|
|
122
|
+
r.headers.authorization = 'Bearer ' + token.accessToken;
|
|
123
|
+
|
|
124
|
+
await expect(testServer.test(endpoint, r)).rejects.toThrow(/Invalid signature for file/);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('Can set signed private files', async () => {
|
|
128
|
+
const organization = await createOrganization();
|
|
129
|
+
|
|
130
|
+
const user = await new UserFactory({
|
|
131
|
+
organization,
|
|
132
|
+
permissions: Permissions.create({
|
|
133
|
+
level: PermissionLevel.Full,
|
|
134
|
+
}),
|
|
135
|
+
}).create();
|
|
136
|
+
const token = await Token.createToken(user);
|
|
137
|
+
|
|
138
|
+
// Create a user member
|
|
139
|
+
const member = MemberWithRegistrationsBlob.create({
|
|
140
|
+
details: MemberDetails.create({
|
|
141
|
+
firstName: 'John',
|
|
142
|
+
lastName: 'Doe',
|
|
143
|
+
}),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// A malicious user could try to set a private file to a member in order to get
|
|
147
|
+
// access to the signed URL. This should not be possible without a valid signature
|
|
148
|
+
const privateFile = new File({
|
|
149
|
+
id: 'test',
|
|
150
|
+
server: 'test.com',
|
|
151
|
+
path: 'test.txt',
|
|
152
|
+
size: 100,
|
|
153
|
+
isPrivate: true,
|
|
154
|
+
});
|
|
155
|
+
await privateFile.sign();
|
|
156
|
+
|
|
157
|
+
member.details.recordAnswers.set(recordSettings.id, RecordFileAnswer.create({
|
|
158
|
+
file: privateFile,
|
|
159
|
+
settings: recordSettings,
|
|
160
|
+
}));
|
|
161
|
+
|
|
162
|
+
const arr = new PatchableArray() as PatchableArrayAutoEncoder<MemberWithRegistrationsBlob>;
|
|
163
|
+
arr.addPut(member);
|
|
164
|
+
|
|
165
|
+
const r = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
|
|
166
|
+
r.headers.authorization = 'Bearer ' + token.accessToken;
|
|
167
|
+
|
|
168
|
+
const response = await testServer.test(endpoint, r);
|
|
169
|
+
expect(response.body).toBeDefined();
|
|
170
|
+
|
|
171
|
+
// Check file has a signed URL in the response
|
|
172
|
+
const memberStruct = response.body.members[0];
|
|
173
|
+
const answer = memberStruct.details.recordAnswers.get(recordSettings.id);
|
|
174
|
+
if (!answer) {
|
|
175
|
+
// eslint-disable-next-line jest/no-conditional-expect
|
|
176
|
+
expect(memberStruct.details.recordAnswers).toHaveProperty(recordSettings.id);
|
|
177
|
+
throw new Error('Unexpected: Answer is not defined');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!(answer instanceof RecordFileAnswer)) {
|
|
181
|
+
throw new Error('Unexpected: Answer is not a RecordFileAnswer');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
expect(answer.file).toBeDefined();
|
|
185
|
+
expect(answer.file!.isPrivate).toBe(true);
|
|
186
|
+
expect(answer.file!.signature).toBeTruthy();
|
|
187
|
+
expect(answer.file!.signedUrl).toBeTruthy();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('Can set public files', async () => {
|
|
191
|
+
const organization = await createOrganization();
|
|
192
|
+
|
|
193
|
+
const user = await new UserFactory({
|
|
194
|
+
organization,
|
|
195
|
+
permissions: Permissions.create({
|
|
196
|
+
level: PermissionLevel.Full,
|
|
197
|
+
}),
|
|
198
|
+
}).create();
|
|
199
|
+
const token = await Token.createToken(user);
|
|
200
|
+
|
|
201
|
+
// Create a user member
|
|
202
|
+
const member = MemberWithRegistrationsBlob.create({
|
|
203
|
+
details: MemberDetails.create({
|
|
204
|
+
firstName: 'John',
|
|
205
|
+
lastName: 'Doe',
|
|
206
|
+
}),
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// A malicious user could try to set a private file to a member in order to get
|
|
210
|
+
// access to the signed URL. This should not be possible without a valid signature
|
|
211
|
+
const publicFile = new File({
|
|
212
|
+
id: 'test',
|
|
213
|
+
server: 'test.com',
|
|
214
|
+
path: 'test.txt',
|
|
215
|
+
size: 100,
|
|
216
|
+
isPrivate: false,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
member.details.recordAnswers.set(recordSettings.id, RecordFileAnswer.create({
|
|
220
|
+
file: publicFile,
|
|
221
|
+
settings: recordSettings,
|
|
222
|
+
}));
|
|
223
|
+
|
|
224
|
+
const arr = new PatchableArray() as PatchableArrayAutoEncoder<MemberWithRegistrationsBlob>;
|
|
225
|
+
arr.addPut(member);
|
|
226
|
+
|
|
227
|
+
const r = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
|
|
228
|
+
r.headers.authorization = 'Bearer ' + token.accessToken;
|
|
229
|
+
|
|
230
|
+
const response = await testServer.test(endpoint, r);
|
|
231
|
+
expect(response.body).toBeDefined();
|
|
232
|
+
|
|
233
|
+
// Check file has not signed URL
|
|
234
|
+
const memberStruct = response.body.members[0];
|
|
235
|
+
const answer = memberStruct.details.recordAnswers.get(recordSettings.id);
|
|
236
|
+
if (!answer) {
|
|
237
|
+
// eslint-disable-next-line jest/no-conditional-expect
|
|
238
|
+
expect(memberStruct.details.recordAnswers).toHaveProperty(recordSettings.id);
|
|
239
|
+
throw new Error('Unexpected: Answer is not defined');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!(answer instanceof RecordFileAnswer)) {
|
|
243
|
+
throw new Error('Unexpected: Answer is not a RecordFileAnswer');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
expect(answer.file).toBeDefined();
|
|
247
|
+
expect(answer.file!.isPrivate).toBe(false);
|
|
248
|
+
expect(answer.file!.signature).toBeFalsy();
|
|
249
|
+
expect(answer.file!.signedUrl).toBeFalsy();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Tests that when an unverified file is stored on the server (in case someone managed to bypass upload security),
|
|
254
|
+
* the server will never generate a signed URL for it.
|
|
255
|
+
*/
|
|
256
|
+
test('Does not generate signed urls for unverifiable files', async () => {
|
|
257
|
+
const organization = await createOrganization();
|
|
258
|
+
|
|
259
|
+
const user = await new UserFactory({
|
|
260
|
+
organization,
|
|
261
|
+
permissions: Permissions.create({
|
|
262
|
+
level: PermissionLevel.Full,
|
|
263
|
+
}),
|
|
264
|
+
}).create();
|
|
265
|
+
const token = await Token.createToken(user);
|
|
266
|
+
|
|
267
|
+
// Create a user member
|
|
268
|
+
const member = MemberWithRegistrationsBlob.create({
|
|
269
|
+
details: MemberDetails.create({
|
|
270
|
+
firstName: 'John',
|
|
271
|
+
lastName: 'Doe',
|
|
272
|
+
}),
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// A malicious user could try to set a private file to a member in order to get
|
|
276
|
+
// access to the signed URL. This should not be possible without a valid signature
|
|
277
|
+
const maliciousFile = new File({
|
|
278
|
+
id: 'test',
|
|
279
|
+
server: 'test.com',
|
|
280
|
+
path: 'test.txt',
|
|
281
|
+
size: 100,
|
|
282
|
+
isPrivate: true,
|
|
283
|
+
signature: 'invalid',
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const arr = new PatchableArray() as PatchableArrayAutoEncoder<MemberWithRegistrationsBlob>;
|
|
287
|
+
arr.addPut(member);
|
|
288
|
+
|
|
289
|
+
const r = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
|
|
290
|
+
r.headers.authorization = 'Bearer ' + token.accessToken;
|
|
291
|
+
|
|
292
|
+
const response = await testServer.test(endpoint, r);
|
|
293
|
+
expect(response.body).toBeDefined();
|
|
294
|
+
|
|
295
|
+
const memberId = response.body.members[0].id;
|
|
296
|
+
|
|
297
|
+
// Do a direct change in the database to simulate a file that was uploaded without a signature
|
|
298
|
+
const model = await Member.getByID(memberId);
|
|
299
|
+
if (!model) {
|
|
300
|
+
throw new Error('Member not found');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
model.details.recordAnswers.set(recordSettings.id, RecordFileAnswer.create({
|
|
304
|
+
file: maliciousFile,
|
|
305
|
+
settings: recordSettings,
|
|
306
|
+
}));
|
|
307
|
+
|
|
308
|
+
await model.save();
|
|
309
|
+
|
|
310
|
+
// Now try to fetch the member again using the API
|
|
311
|
+
const r2 = Request.buildJson('GET', '/user/members', organization.getApiHost());
|
|
312
|
+
r2.headers.authorization = 'Bearer ' + token.accessToken;
|
|
313
|
+
|
|
314
|
+
const response2 = await testServer.test(getMembersEndpoint, r2);
|
|
315
|
+
expect(response2.body).toBeDefined();
|
|
316
|
+
|
|
317
|
+
// Check file has not signed URL
|
|
318
|
+
const memberStruct = response2.body.members[0];
|
|
319
|
+
const answer = memberStruct.details.recordAnswers.get(recordSettings.id);
|
|
320
|
+
if (!answer) {
|
|
321
|
+
throw new Error('Unexpected: Answer is not defined but expected');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (!(answer instanceof RecordFileAnswer)) {
|
|
325
|
+
throw new Error('Unexpected: Answer is not a RecordFileAnswer');
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
expect(answer.file).toBeDefined();
|
|
329
|
+
expect(answer.file!.isPrivate).toBe(true);
|
|
330
|
+
expect(answer.file!.signedUrl).toBeFalsy();
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Tests that when an verified file is stored on the server with a signed url, the server will never return that signed url.
|
|
335
|
+
*/
|
|
336
|
+
test('Does not return stored signed urls from database', async () => {
|
|
337
|
+
const organization = await createOrganization();
|
|
338
|
+
|
|
339
|
+
const user = await new UserFactory({
|
|
340
|
+
organization,
|
|
341
|
+
permissions: Permissions.create({
|
|
342
|
+
level: PermissionLevel.Full,
|
|
343
|
+
}),
|
|
344
|
+
}).create();
|
|
345
|
+
const token = await Token.createToken(user);
|
|
346
|
+
|
|
347
|
+
// Create a user member
|
|
348
|
+
const member = MemberWithRegistrationsBlob.create({
|
|
349
|
+
details: MemberDetails.create({
|
|
350
|
+
firstName: 'John',
|
|
351
|
+
lastName: 'Doe',
|
|
352
|
+
}),
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// A malicious user could try to set a private file to a member in order to get
|
|
356
|
+
// access to the signed URL. This should not be possible without a valid signature
|
|
357
|
+
const maliciousFile = new File({
|
|
358
|
+
id: 'test',
|
|
359
|
+
server: 'test.com',
|
|
360
|
+
path: 'test.txt',
|
|
361
|
+
size: 100,
|
|
362
|
+
isPrivate: true,
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
await maliciousFile.sign();
|
|
366
|
+
maliciousFile.signedUrl = 'https://test.com/test.exe';
|
|
367
|
+
|
|
368
|
+
const arr = new PatchableArray() as PatchableArrayAutoEncoder<MemberWithRegistrationsBlob>;
|
|
369
|
+
arr.addPut(member);
|
|
370
|
+
|
|
371
|
+
const r = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
|
|
372
|
+
r.headers.authorization = 'Bearer ' + token.accessToken;
|
|
373
|
+
|
|
374
|
+
const response = await testServer.test(endpoint, r);
|
|
375
|
+
expect(response.body).toBeDefined();
|
|
376
|
+
|
|
377
|
+
const memberId = response.body.members[0].id;
|
|
378
|
+
|
|
379
|
+
// Do a direct change in the database to simulate a file that was uploaded without a signature
|
|
380
|
+
const model = await Member.getByID(memberId);
|
|
381
|
+
if (!model) {
|
|
382
|
+
throw new Error('Member not found');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
model.details.recordAnswers.set(recordSettings.id, RecordFileAnswer.create({
|
|
386
|
+
file: maliciousFile,
|
|
387
|
+
settings: recordSettings,
|
|
388
|
+
}));
|
|
389
|
+
|
|
390
|
+
await model.save();
|
|
391
|
+
|
|
392
|
+
// Now try to fetch the member again using the API
|
|
393
|
+
const r2 = Request.buildJson('GET', '/user/members', organization.getApiHost());
|
|
394
|
+
r2.headers.authorization = 'Bearer ' + token.accessToken;
|
|
395
|
+
|
|
396
|
+
const response2 = await testServer.test(getMembersEndpoint, r2);
|
|
397
|
+
expect(response2.body).toBeDefined();
|
|
398
|
+
|
|
399
|
+
// Check file has not signed URL
|
|
400
|
+
const memberStruct = response2.body.members[0];
|
|
401
|
+
const answer = memberStruct.details.recordAnswers.get(recordSettings.id);
|
|
402
|
+
if (!answer) {
|
|
403
|
+
throw new Error('Unexpected: Answer is not defined but expected');
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (!(answer instanceof RecordFileAnswer)) {
|
|
407
|
+
throw new Error('Unexpected: Answer is not a RecordFileAnswer');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
expect(answer.file).toBeDefined();
|
|
411
|
+
expect(answer.file!.isPrivate).toBe(true);
|
|
412
|
+
expect(answer.file!.signedUrl).toBeString();
|
|
413
|
+
expect(answer.file!.signedUrl).not.toEqual('https://test.com/test.exe'); // It got replaced with a proper signed url
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Tests that when an unverified file is stored on the server with a signed url, the server will never return that signed url.
|
|
418
|
+
*/
|
|
419
|
+
test('Does not return stored signed urls from database for unverified files', async () => {
|
|
420
|
+
const organization = await createOrganization();
|
|
421
|
+
|
|
422
|
+
const user = await new UserFactory({
|
|
423
|
+
organization,
|
|
424
|
+
permissions: Permissions.create({
|
|
425
|
+
level: PermissionLevel.Full,
|
|
426
|
+
}),
|
|
427
|
+
}).create();
|
|
428
|
+
const token = await Token.createToken(user);
|
|
429
|
+
|
|
430
|
+
// Create a user member
|
|
431
|
+
const member = MemberWithRegistrationsBlob.create({
|
|
432
|
+
details: MemberDetails.create({
|
|
433
|
+
firstName: 'John',
|
|
434
|
+
lastName: 'Doe',
|
|
435
|
+
}),
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// A malicious user could try to set a private file to a member in order to get
|
|
439
|
+
// access to the signed URL. This should not be possible without a valid signature
|
|
440
|
+
const maliciousFile = new File({
|
|
441
|
+
id: 'test',
|
|
442
|
+
server: 'test.com',
|
|
443
|
+
path: 'test.txt',
|
|
444
|
+
size: 100,
|
|
445
|
+
isPrivate: true,
|
|
446
|
+
signature: 'invalid',
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
maliciousFile.signedUrl = 'https://test.com/test.exe';
|
|
450
|
+
|
|
451
|
+
const arr = new PatchableArray() as PatchableArrayAutoEncoder<MemberWithRegistrationsBlob>;
|
|
452
|
+
arr.addPut(member);
|
|
453
|
+
|
|
454
|
+
const r = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
|
|
455
|
+
r.headers.authorization = 'Bearer ' + token.accessToken;
|
|
456
|
+
|
|
457
|
+
const response = await testServer.test(endpoint, r);
|
|
458
|
+
expect(response.body).toBeDefined();
|
|
459
|
+
|
|
460
|
+
const memberId = response.body.members[0].id;
|
|
461
|
+
|
|
462
|
+
// Do a direct change in the database to simulate a file that was uploaded without a signature
|
|
463
|
+
const model = await Member.getByID(memberId);
|
|
464
|
+
if (!model) {
|
|
465
|
+
throw new Error('Member not found');
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
model.details.recordAnswers.set(recordSettings.id, RecordFileAnswer.create({
|
|
469
|
+
file: maliciousFile,
|
|
470
|
+
settings: recordSettings,
|
|
471
|
+
}));
|
|
472
|
+
|
|
473
|
+
await model.save();
|
|
474
|
+
|
|
475
|
+
// Now try to fetch the member again using the API
|
|
476
|
+
const r2 = Request.buildJson('GET', '/user/members', organization.getApiHost());
|
|
477
|
+
r2.headers.authorization = 'Bearer ' + token.accessToken;
|
|
478
|
+
|
|
479
|
+
const response2 = await testServer.test(getMembersEndpoint, r2);
|
|
480
|
+
expect(response2.body).toBeDefined();
|
|
481
|
+
|
|
482
|
+
// Check file has not signed URL
|
|
483
|
+
const memberStruct = response2.body.members[0];
|
|
484
|
+
const answer = memberStruct.details.recordAnswers.get(recordSettings.id);
|
|
485
|
+
if (!answer) {
|
|
486
|
+
throw new Error('Unexpected: Answer is not defined but expected');
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (!(answer instanceof RecordFileAnswer)) {
|
|
490
|
+
throw new Error('Unexpected: Answer is not a RecordFileAnswer');
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
expect(answer.file).toBeDefined();
|
|
494
|
+
expect(answer.file!.isPrivate).toBe(true);
|
|
495
|
+
expect(answer.file!.signedUrl).toBeNull();
|
|
496
|
+
});
|
|
497
|
+
});
|