@stamhoofd/backend 2.79.1 → 2.79.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stamhoofd/backend",
3
- "version": "2.79.1",
3
+ "version": "2.79.2",
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.1",
42
- "@stamhoofd/backend-middleware": "2.79.1",
43
- "@stamhoofd/email": "2.79.1",
44
- "@stamhoofd/models": "2.79.1",
45
- "@stamhoofd/queues": "2.79.1",
46
- "@stamhoofd/sql": "2.79.1",
47
- "@stamhoofd/structures": "2.79.1",
48
- "@stamhoofd/utility": "2.79.1",
41
+ "@stamhoofd/backend-i18n": "2.79.2",
42
+ "@stamhoofd/backend-middleware": "2.79.2",
43
+ "@stamhoofd/email": "2.79.2",
44
+ "@stamhoofd/models": "2.79.2",
45
+ "@stamhoofd/queues": "2.79.2",
46
+ "@stamhoofd/sql": "2.79.2",
47
+ "@stamhoofd/structures": "2.79.2",
48
+ "@stamhoofd/utility": "2.79.2",
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": "88ae0a3e4edb57d36151c551a4c4e3ed59447e05"
68
+ "gitHead": "595b8c9ed42fb1b9de1dd3facb545ae3c7f9b1a2"
69
69
  }
@@ -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 (!await Context.auth.hasFullAccess(organizationId)) {
447
- if (allowedIds.includes(struct.id)) {
448
- // Ok
449
- }
450
- else {
451
- throw Context.auth.error('Je hebt geen toegangsrechten om groepen toe te voegen');
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
- // Check if current user has permissions to this new group -> else fail with error
499
- if (!await Context.auth.canAccessGroup(model, PermissionLevel.Full)) {
500
- throw new SimpleError({
501
- code: 'missing_permissions',
502
- message: 'You cannot restrict your own permissions',
503
- human: 'Je kan geen inschrijvingsgroep maken zonder dat je zelf volledige toegang hebt tot de nieuwe groep',
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
  }
@@ -195,11 +195,13 @@ export class AdminPermissionChecker {
195
195
  }
196
196
 
197
197
  // Check parent categories
198
- const organizationPeriod = await this.getOrganizationCurrentPeriod(organization);
199
- const parentCategories = group.getParentCategories(organizationPeriod.settings.categories);
200
- for (const category of parentCategories) {
201
- if (organizationPermissions.hasResourceAccess(PermissionsResourceType.GroupCategories, category.id, permissionLevel)) {
202
- return true;
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
- console.error('Failed to verify file signature:', e);
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
  */