@nymphjs/tilmeld 1.0.0-alpha.4 → 1.0.0-alpha.42

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/src/Group.ts CHANGED
@@ -106,9 +106,9 @@ export default class Group extends AbleObject<GroupData> {
106
106
  'getPrimaryGroups',
107
107
  'getSecondaryGroups',
108
108
  ];
109
- protected $privateData = Group.DEFAULT_PRIVATE_DATA;
110
- public static searchRestrictedData = Group.DEFAULT_PRIVATE_DATA;
111
- protected $allowlistData? = Group.DEFAULT_ALLOWLIST_DATA;
109
+ protected $privateData = [...Group.DEFAULT_PRIVATE_DATA];
110
+ public static searchRestrictedData = [...Group.DEFAULT_PRIVATE_DATA];
111
+ protected $allowlistData? = [...Group.DEFAULT_ALLOWLIST_DATA];
112
112
  protected $allowlistTags?: string[] = [];
113
113
 
114
114
  /**
@@ -294,8 +294,8 @@ export default class Group extends AbleObject<GroupData> {
294
294
  const tilmeld = this.$nymph.tilmeld as Tilmeld;
295
295
  let user = givenUser ?? tilmeld.User.current();
296
296
 
297
- this.$privateData = Group.DEFAULT_PRIVATE_DATA;
298
- this.$allowlistData = Group.DEFAULT_ALLOWLIST_DATA;
297
+ this.$privateData = [...Group.DEFAULT_PRIVATE_DATA];
298
+ this.$allowlistData = [...Group.DEFAULT_ALLOWLIST_DATA];
299
299
 
300
300
  if (tilmeld.config.emailUsernames) {
301
301
  this.$privateData.push('groupname');
@@ -746,13 +746,16 @@ export default class Group extends AbleObject<GroupData> {
746
746
  }
747
747
 
748
748
  public async $delete() {
749
- const tilmeld = this.$nymph.tilmeld as Tilmeld;
749
+ let tilmeld = this.$nymph.tilmeld as Tilmeld;
750
750
  if (!tilmeld.gatekeeper('tilmeld/admin')) {
751
751
  throw new BadDataError("You don't have the authority to delete groups.");
752
752
  }
753
753
 
754
754
  const transaction = 'tilmeld-delete-' + this.guid;
755
- const tnymph = await this.$nymph.startTransaction(transaction);
755
+ const nymph = this.$nymph;
756
+ const tnymph = await nymph.startTransaction(transaction);
757
+ this.$nymph = tnymph;
758
+ tilmeld = this.$nymph.tilmeld as Tilmeld;
756
759
 
757
760
  // Delete descendants.
758
761
  const descendants = await this.$getDescendants();
@@ -760,6 +763,7 @@ export default class Group extends AbleObject<GroupData> {
760
763
  for (let curGroup of descendants) {
761
764
  if (!(await curGroup.$delete())) {
762
765
  await tnymph.rollback(transaction);
766
+ this.$nymph = nymph;
763
767
  return false;
764
768
  }
765
769
  }
@@ -780,6 +784,7 @@ export default class Group extends AbleObject<GroupData> {
780
784
  delete user.group;
781
785
  if (!(await user.$save())) {
782
786
  await tnymph.rollback(transaction);
787
+ this.$nymph = nymph;
783
788
  return false;
784
789
  }
785
790
  }
@@ -810,6 +815,7 @@ export default class Group extends AbleObject<GroupData> {
810
815
  } else {
811
816
  await tnymph.rollback(transaction);
812
817
  }
818
+ this.$nymph = nymph;
813
819
  return success;
814
820
  }
815
821
  }
package/src/Tilmeld.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  TilmeldAccessLevels,
8
8
  } from '@nymphjs/nymph';
9
9
  import { Request, Response } from 'express';
10
+ import { xor } from 'lodash';
10
11
 
11
12
  import { Config, ConfigDefaults as defaults } from './conf';
12
13
  import { AccessControlError } from './errors';
@@ -147,14 +148,13 @@ export default class Tilmeld implements TilmeldInterface {
147
148
  }
148
149
 
149
150
  private initAccessControl() {
150
- const tilmeld = this;
151
-
152
151
  // Check for the skip access control option and add AC selectors.
153
152
  const handleQuery = function (
154
- _nymph: Nymph,
153
+ nymph: Nymph,
155
154
  options: Options,
156
155
  selectors: FormattedSelector[]
157
156
  ) {
157
+ const tilmeld = nymph.tilmeld as Tilmeld;
158
158
  if (
159
159
  !options ||
160
160
  !('skipAc' in options) ||
@@ -191,9 +191,10 @@ export default class Tilmeld implements TilmeldInterface {
191
191
 
192
192
  // Filter entities being deleted for user permissions.
193
193
  const checkPermissionsDelete = function (
194
- _nymph: Nymph,
194
+ nymph: Nymph,
195
195
  entity: EntityInterface
196
196
  ) {
197
+ const tilmeld = nymph.tilmeld as Tilmeld;
197
198
  if (
198
199
  typeof entity.$tilmeldDeleteSkipAC === 'function' &&
199
200
  entity.$tilmeldDeleteSkipAC()
@@ -201,7 +202,7 @@ export default class Tilmeld implements TilmeldInterface {
201
202
  return;
202
203
  }
203
204
  // Test for permissions.
204
- if (tilmeld.checkPermissions(entity, TilmeldAccessLevels.FULL_ACCESS)) {
205
+ if (!tilmeld.checkPermissions(entity, TilmeldAccessLevels.FULL_ACCESS)) {
205
206
  throw new AccessControlError('No permission to delete.');
206
207
  }
207
208
  };
@@ -213,8 +214,8 @@ export default class Tilmeld implements TilmeldInterface {
213
214
  guid: string,
214
215
  className?: string
215
216
  ) {
216
- const entity = tilmeld.nymph.driver.getEntitySync(
217
- { class: tilmeld.nymph.getEntityClass(className ?? 'Entity') },
217
+ const entity = nymph.driver.getEntitySync(
218
+ { class: nymph.getEntityClass(className ?? 'Entity') },
218
219
  { type: '&', guid: guid }
219
220
  );
220
221
  if (entity != null) {
@@ -226,9 +227,10 @@ export default class Tilmeld implements TilmeldInterface {
226
227
  // Filter entities being saved for user permissions, and filter any
227
228
  // disallowed changes to AC properties.
228
229
  const checkPermissionsSaveAndFilterAcChanges = function (
229
- _nymph: Nymph,
230
+ nymph: Nymph,
230
231
  entity: EntityInterface & AccessControlData
231
232
  ) {
233
+ const tilmeld = nymph.tilmeld as Tilmeld;
232
234
  if (!entity) {
233
235
  return;
234
236
  }
@@ -244,15 +246,15 @@ export default class Tilmeld implements TilmeldInterface {
244
246
  // allowing a change to ac properties.
245
247
 
246
248
  const originalAc = entity.$getOriginalAcValues();
247
- const newAc = {
248
- user: entity.user ?? null,
249
- group: entity.group ?? null,
250
- acUser: entity.acUser ?? null,
251
- acGroup: entity.acGroup ?? null,
252
- acOther: entity.acOther ?? null,
253
- acRead: entity.acRead ?? null,
254
- acWrite: entity.acWrite ?? null,
255
- acFull: entity.acFull ?? null,
249
+ const newAc: AccessControlData = {
250
+ user: entity.user ?? undefined,
251
+ group: entity.group ?? undefined,
252
+ acUser: entity.acUser ?? undefined,
253
+ acGroup: entity.acGroup ?? undefined,
254
+ acOther: entity.acOther ?? undefined,
255
+ acRead: entity.acRead ?? undefined,
256
+ acWrite: entity.acWrite ?? undefined,
257
+ acFull: entity.acFull ?? undefined,
256
258
  };
257
259
 
258
260
  const setAcProperties = (acValues: { [k: string]: any }) => {
@@ -268,10 +270,102 @@ export default class Tilmeld implements TilmeldInterface {
268
270
 
269
271
  // Restore original AC properties and check permissions.
270
272
  setAcProperties(originalAc);
271
- if (tilmeld.checkPermissions(entity, TilmeldAccessLevels.FULL_ACCESS)) {
272
- // Only allow changes to AC properties if the user has full access.
273
- // TODO: only allow changes to `user` and `group` if tilmeld admin or
274
- // group is user's group.
273
+
274
+ function areAcPropertiesChanged(
275
+ a: AccessControlData,
276
+ b: AccessControlData
277
+ ) {
278
+ for (const name of ['user', 'group'] as ('user' | 'group')[]) {
279
+ const aVal = a[name];
280
+ const bVal = b[name];
281
+ if (aVal == null && bVal == null) {
282
+ continue;
283
+ } else if (aVal == null || bVal == null) {
284
+ return true;
285
+ }
286
+ if (!aVal.$is(bVal)) {
287
+ return true;
288
+ }
289
+ }
290
+ for (const name of ['acUser', 'acGroup', 'acOther'] as (
291
+ | 'acUser'
292
+ | 'acGroup'
293
+ | 'acOther'
294
+ )[]) {
295
+ const aVal = a[name];
296
+ const bVal = b[name];
297
+ if (aVal == null && bVal == null) {
298
+ continue;
299
+ } else if (aVal == null || bVal == null) {
300
+ return true;
301
+ }
302
+ if (aVal !== bVal) {
303
+ return true;
304
+ }
305
+ }
306
+ for (const name of ['acRead', 'acWrite', 'acFull'] as (
307
+ | 'acRead'
308
+ | 'acWrite'
309
+ | 'acFull'
310
+ )[]) {
311
+ const aVal = a[name];
312
+ const bVal = b[name];
313
+ if (aVal == null && bVal == null) {
314
+ continue;
315
+ } else if (aVal == null || bVal == null) {
316
+ return true;
317
+ }
318
+ if (aVal.length !== bVal.length) {
319
+ return true;
320
+ }
321
+ if (
322
+ xor(
323
+ aVal.map((entity) => entity.guid),
324
+ bVal.map((entity) => entity.guid)
325
+ ).length
326
+ ) {
327
+ return true;
328
+ }
329
+ }
330
+ return false;
331
+ }
332
+
333
+ if (areAcPropertiesChanged(originalAc, newAc)) {
334
+ if (
335
+ !tilmeld.checkPermissions(entity, TilmeldAccessLevels.FULL_ACCESS)
336
+ ) {
337
+ // Only allow changes to AC properties if the user has full access.
338
+ throw new AccessControlError(
339
+ 'No permission to change access control properties.'
340
+ );
341
+ }
342
+
343
+ if (
344
+ ((originalAc.user && !originalAc.user.$is(newAc.user)) ||
345
+ (!originalAc.user &&
346
+ newAc.user &&
347
+ !newAc.user.$is(tilmeld.currentUser))) &&
348
+ !tilmeld.gatekeeper('tilmeld/admin')
349
+ ) {
350
+ throw new AccessControlError(
351
+ 'No permission to assign to another user.'
352
+ );
353
+ }
354
+
355
+ if (
356
+ newAc.group &&
357
+ !newAc.group.$is(originalAc.group) &&
358
+ !(
359
+ newAc.group.$is(tilmeld.currentUser?.group) ||
360
+ newAc.group.$inArray(tilmeld.currentUser?.groups ?? [])
361
+ ) &&
362
+ !tilmeld.gatekeeper('tilmeld/admin')
363
+ ) {
364
+ throw new AccessControlError(
365
+ 'No permission to assign to another group.'
366
+ );
367
+ }
368
+
275
369
  setAcProperties(newAc);
276
370
  }
277
371
  }
@@ -300,9 +394,10 @@ export default class Tilmeld implements TilmeldInterface {
300
394
  * - acOther = TilmeldAccessLevels.NO_ACCESS
301
395
  */
302
396
  const addAccess = function (
303
- _nymph: Nymph,
397
+ nymph: Nymph,
304
398
  entity: EntityInterface & AccessControlData
305
399
  ) {
400
+ const tilmeld = nymph.tilmeld as Tilmeld;
306
401
  const user = tilmeld.currentUser;
307
402
  if (
308
403
  user != null &&
@@ -353,30 +448,40 @@ export default class Tilmeld implements TilmeldInterface {
353
448
  prop < TilmeldAccessLevels.NO_ACCESS ||
354
449
  prop > TilmeldAccessLevels.FULL_ACCESS
355
450
  ) {
356
- throw new AccessControlError('Invalid access control property.');
451
+ throw new AccessControlError(
452
+ 'Invalid access control property: ' + prop
453
+ );
357
454
  }
358
455
  };
359
456
 
360
457
  const accessAcPropertyValidator = (prop: any) => {
361
458
  if (!Array.isArray(prop)) {
362
- throw new AccessControlError('Invalid access control property.');
459
+ throw new AccessControlError(
460
+ 'Invalid access control property: ' + prop
461
+ );
363
462
  }
364
463
  prop.forEach((value) => {
365
464
  if (!(value instanceof User || value instanceof Group)) {
366
- throw new AccessControlError('Invalid access control property.');
465
+ throw new AccessControlError(
466
+ 'Invalid access control property: ' + prop
467
+ );
367
468
  }
368
469
  });
369
470
  };
370
471
 
371
472
  if ('user' in entity) {
372
473
  if (!(entity.user instanceof User)) {
373
- throw new AccessControlError('Invalid access control property.');
474
+ throw new AccessControlError(
475
+ 'Invalid access control property: user'
476
+ );
374
477
  }
375
478
  }
376
479
 
377
480
  if ('group' in entity) {
378
- if (!(entity.Group instanceof Group)) {
379
- throw new AccessControlError('Invalid access control property.');
481
+ if (!(entity.group instanceof Group)) {
482
+ throw new AccessControlError(
483
+ 'Invalid access control property: group'
484
+ );
380
485
  }
381
486
  }
382
487
 
@@ -443,7 +548,7 @@ export default class Tilmeld implements TilmeldInterface {
443
548
  return;
444
549
  }
445
550
 
446
- if (user === null) {
551
+ if (user == null || user.guid == null) {
447
552
  selectors.push({
448
553
  type: '|',
449
554
  // Other access control is sufficient.
@@ -452,7 +557,7 @@ export default class Tilmeld implements TilmeldInterface {
452
557
  selector: [
453
558
  {
454
559
  type: '&',
455
- '!defined': ['user', 'group'],
560
+ '!defined': [['user', 'group']],
456
561
  },
457
562
  ],
458
563
  });
@@ -460,56 +565,43 @@ export default class Tilmeld implements TilmeldInterface {
460
565
  const subSelectors: FormattedSelector[] = [
461
566
  {
462
567
  type: '&',
463
- '!defined': ['user', 'group'],
568
+ '!defined': [['user', 'group']],
464
569
  },
465
570
  // It is owned by the user.
466
571
  {
467
572
  type: '&',
468
- ref: [['user', user]],
573
+ ref: [['user', user.guid]],
469
574
  gte: [['acUser', TilmeldAccessLevels.READ_ACCESS]],
470
575
  },
471
576
  ];
472
- const selector: FormattedSelector = {
473
- type: '|',
474
- // Other access control is sufficient.
475
- gte: [['acOther', TilmeldAccessLevels.READ_ACCESS]],
476
- // The user and group are not set.
477
- selector: subSelectors,
478
- // The user is listed in acRead, acWrite, or acFull.
479
- ref: [
480
- ['acRead', user],
481
- ['acWrite', user],
482
- ['acFull', user],
483
- ],
484
- };
485
577
  const groupRefs: FormattedSelector['ref'] = [];
486
578
  const acRefs: FormattedSelector['ref'] = [];
487
579
  if (user.group != null && user.group.guid != null) {
488
580
  // It belongs to the user's primary group.
489
- groupRefs.push(['group', user.group]);
581
+ groupRefs.push(['group', user.group.guid]);
490
582
  // User's primary group is listed in acRead, acWrite, or acFull.
491
- acRefs.push(['acRead', user.group]);
492
- acRefs.push(['acWrite', user.group]);
493
- acRefs.push(['acFull', user.group]);
583
+ acRefs.push(['acRead', user.group.guid]);
584
+ acRefs.push(['acWrite', user.group.guid]);
585
+ acRefs.push(['acFull', user.group.guid]);
494
586
  }
495
587
  for (let curSecondaryGroup of user.groups ?? []) {
496
588
  if (curSecondaryGroup != null && curSecondaryGroup.guid != null) {
497
589
  // It belongs to the user's secondary group.
498
- groupRefs.push(['group', curSecondaryGroup]);
590
+ groupRefs.push(['group', curSecondaryGroup.guid]);
499
591
  // User's secondary group is listed in acRead, acWrite, or acFull.
500
- acRefs.push(['acRead', curSecondaryGroup]);
501
- acRefs.push(['acWrite', curSecondaryGroup]);
502
- acRefs.push(['acFull', curSecondaryGroup]);
592
+ acRefs.push(['acRead', curSecondaryGroup.guid]);
593
+ acRefs.push(['acWrite', curSecondaryGroup.guid]);
594
+ acRefs.push(['acFull', curSecondaryGroup.guid]);
503
595
  }
504
596
  }
505
597
  for (let curDescendantGroup of user.$getDescendantGroupsSync()) {
506
598
  if (curDescendantGroup != null && curDescendantGroup.guid != null) {
507
599
  // It belongs to the user's secondary group.
508
- groupRefs.push(['group', curDescendantGroup]);
600
+ groupRefs.push(['group', curDescendantGroup.guid]);
509
601
  // User's secondary group is listed in acRead, acWrite, or acFull.
510
- acRefs.push(['acRead', curDescendantGroup]);
511
- acRefs.push(['acWrite', curDescendantGroup]);
512
- acRefs.push(['acFull', curDescendantGroup]);
602
+ acRefs.push(['acRead', curDescendantGroup.guid]);
603
+ acRefs.push(['acWrite', curDescendantGroup.guid]);
604
+ acRefs.push(['acFull', curDescendantGroup.guid]);
513
605
  }
514
606
  }
515
607
  // All the group refs.
@@ -532,6 +624,19 @@ export default class Tilmeld implements TilmeldInterface {
532
624
  ref: acRefs,
533
625
  });
534
626
  }
627
+ const selector: FormattedSelector = {
628
+ type: '|',
629
+ // Other access control is sufficient.
630
+ gte: [['acOther', TilmeldAccessLevels.READ_ACCESS]],
631
+ // The user and group are not set.
632
+ selector: subSelectors,
633
+ // The user is listed in acRead, acWrite, or acFull.
634
+ ref: [
635
+ ['acRead', user.guid],
636
+ ['acWrite', user.guid],
637
+ ['acFull', user.guid],
638
+ ],
639
+ };
535
640
  selectors.push(selector);
536
641
  }
537
642
  }
package/src/User.test.ts CHANGED
@@ -40,6 +40,14 @@ describe('User', () => {
40
40
  return newUser;
41
41
  }
42
42
 
43
+ it('new user passes validation', async () => {
44
+ const validUser = await makeNewUser();
45
+
46
+ expect(() => {
47
+ defaults.validatorUser(validUser);
48
+ }).not.toThrow();
49
+ });
50
+
43
51
  it("doesn't allow unknown keys", async () => {
44
52
  const invalidUser = await makeNewUser();
45
53