@nymphjs/tilmeld 1.0.0-beta.8 → 1.0.0-beta.80

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.
Files changed (57) hide show
  1. package/CHANGELOG.md +364 -0
  2. package/README.md +14 -1
  3. package/dist/AbleObject.d.ts +15 -0
  4. package/dist/AbleObject.js +16 -1
  5. package/dist/AbleObject.js.map +1 -1
  6. package/dist/Group.d.ts +130 -10
  7. package/dist/Group.js +311 -129
  8. package/dist/Group.js.map +1 -1
  9. package/dist/Group.test.js +77 -2
  10. package/dist/Group.test.js.map +1 -1
  11. package/dist/Tilmeld.d.ts +237 -9
  12. package/dist/Tilmeld.js +569 -201
  13. package/dist/Tilmeld.js.map +1 -1
  14. package/dist/User.d.ts +344 -16
  15. package/dist/User.js +979 -313
  16. package/dist/User.js.map +1 -1
  17. package/dist/User.test.js +63 -2
  18. package/dist/User.test.js.map +1 -1
  19. package/dist/conf/d.d.ts +250 -3
  20. package/dist/conf/defaults.js +146 -98
  21. package/dist/conf/defaults.js.map +1 -1
  22. package/dist/enforceTilmeld.d.ts +3 -0
  23. package/dist/enforceTilmeld.js +22 -0
  24. package/dist/enforceTilmeld.js.map +1 -0
  25. package/dist/errors/AccessControlError.d.ts +2 -1
  26. package/dist/errors/AccessControlError.js +3 -2
  27. package/dist/errors/AccessControlError.js.map +1 -1
  28. package/dist/errors/BadDataError.d.ts +2 -1
  29. package/dist/errors/BadDataError.js +3 -2
  30. package/dist/errors/BadDataError.js.map +1 -1
  31. package/dist/errors/BadEmailError.d.ts +2 -1
  32. package/dist/errors/BadEmailError.js +3 -2
  33. package/dist/errors/BadEmailError.js.map +1 -1
  34. package/dist/errors/BadUsernameError.d.ts +2 -1
  35. package/dist/errors/BadUsernameError.js +3 -2
  36. package/dist/errors/BadUsernameError.js.map +1 -1
  37. package/dist/index.d.ts +1 -0
  38. package/dist/index.js +1 -0
  39. package/dist/index.js.map +1 -1
  40. package/package.json +30 -26
  41. package/src/AbleObject.ts +2 -2
  42. package/src/Group.test.ts +102 -4
  43. package/src/Group.ts +274 -158
  44. package/src/Tilmeld.ts +383 -249
  45. package/src/User.test.ts +80 -4
  46. package/src/User.ts +1061 -382
  47. package/src/conf/d.ts +26 -13
  48. package/src/conf/defaults.ts +151 -104
  49. package/src/enforceTilmeld.ts +20 -0
  50. package/src/errors/AccessControlError.ts +4 -2
  51. package/src/errors/BadDataError.ts +4 -2
  52. package/src/errors/BadEmailError.ts +4 -2
  53. package/src/errors/BadUsernameError.ts +4 -2
  54. package/src/index.ts +2 -0
  55. package/src/tilmeldJoiProps.ts +9 -9
  56. package/tsconfig.json +3 -3
  57. package/typedoc.json +4 -0
package/src/Tilmeld.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  Options,
6
6
  TilmeldInterface,
7
7
  TilmeldAccessLevels,
8
+ ACProperties,
8
9
  } from '@nymphjs/nymph';
9
10
  import { URL } from 'url';
10
11
  import { Request, Response } from 'express';
@@ -15,6 +16,7 @@ import { AccessControlError } from './errors';
15
16
  import Group from './Group';
16
17
  import User, { UserData } from './User';
17
18
  import { AccessControlData } from './Tilmeld.types';
19
+ import { enforceTilmeld } from './enforceTilmeld';
18
20
 
19
21
  /**
20
22
  * A user and group system for Nymph.js.
@@ -69,9 +71,17 @@ export default class Tilmeld implements TilmeldInterface {
69
71
  public response: Response | null = null;
70
72
 
71
73
  /**
72
- * Skip the authenticate step of initialization.
74
+ * Used to avoid infinite loop.
73
75
  */
74
- private skipAuthenticate = false;
76
+ private alreadyLoggedOutSwitch = false;
77
+
78
+ /**
79
+ * Gatekeeper ability cache.
80
+ *
81
+ * Gatekeeper will cache the user's abilities that it calculates, so it can
82
+ * check faster if that user has been checked before.
83
+ */
84
+ private gatekeeperCache: { [k: string]: true } | null = null;
75
85
 
76
86
  /**
77
87
  * Create a new instance of Tilmeld.
@@ -80,6 +90,24 @@ export default class Tilmeld implements TilmeldInterface {
80
90
  */
81
91
  public constructor(config: Partial<Config> & { jwtSecret: string }) {
82
92
  this.config = { ...defaults, ...config };
93
+
94
+ if (
95
+ this.config.emailUsernames &&
96
+ (!this.config.userFields.includes('email') ||
97
+ !this.config.regFields.includes('email'))
98
+ ) {
99
+ throw new Error(
100
+ 'You must include email in userFields and regFields if you use emailUsernames.',
101
+ );
102
+ }
103
+
104
+ for (let field of this.config.regFields) {
105
+ if (!this.config.userFields.includes(field)) {
106
+ throw new Error(
107
+ `All fields in regFields must also be in userFields: ${field}`,
108
+ );
109
+ }
110
+ }
83
111
  }
84
112
 
85
113
  /**
@@ -92,7 +120,7 @@ export default class Tilmeld implements TilmeldInterface {
92
120
  tilmeld.request = this.request;
93
121
  tilmeld.response = this.response;
94
122
  tilmeld.currentUser = this.currentUser;
95
- tilmeld.skipAuthenticate = true;
123
+ tilmeld.gatekeeperCache = this.gatekeeperCache;
96
124
 
97
125
  return tilmeld;
98
126
  }
@@ -106,32 +134,45 @@ export default class Tilmeld implements TilmeldInterface {
106
134
  */
107
135
  public init(nymph: Nymph) {
108
136
  this.nymph = nymph;
109
- const tilmeld = this;
110
137
 
111
- // Set up access control hooks when Nymph is called.
112
138
  if (this.nymph.driver == null) {
113
139
  throw new Error("Tilmeld can't be configured before Nymph.");
114
140
  }
115
141
 
116
142
  // Configure the classes.
117
- class NymphUser extends User {
118
- static nymph: Nymph = nymph;
119
- static tilmeld: Tilmeld = tilmeld;
143
+ if (nymph.parent) {
144
+ // This is a cloned Nymph, it should have classes already.
145
+ this.User = this.nymph.getEntityClass(User);
146
+ this.Group = this.nymph.getEntityClass(Group);
147
+ } else {
148
+ // This is a new Nymph, add the classes.
149
+ this.User = this.nymph.addEntityClass(User);
150
+ this.Group = this.nymph.addEntityClass(Group);
120
151
  }
121
- (NymphUser as any).skipOnClone = true;
122
- this.User = this.nymph.addEntityClass(NymphUser);
123
- class NymphGroup extends Group {
124
- static nymph: Nymph = nymph;
125
- static tilmeld: Tilmeld = tilmeld;
152
+
153
+ // Clone the current user so its Nymph instance is correct.
154
+ if (this.currentUser) {
155
+ const currentUserGUID = this.currentUser.guid;
156
+ const currentUserCDate = this.currentUser.cdate;
157
+ const currentUserMDate = this.currentUser.mdate;
158
+ const currentUserTags = this.currentUser.tags;
159
+ const currentUserData = this.currentUser.$getData();
160
+ const currentUserSData = this.currentUser.$getSData();
161
+
162
+ const user = this.User.factorySync();
163
+ user.guid = currentUserGUID;
164
+ user.cdate = currentUserCDate;
165
+ user.mdate = currentUserMDate;
166
+ user.tags = currentUserTags;
167
+ user.$putData(currentUserData, currentUserSData);
168
+ this.currentUser = user;
169
+ this.currentUser.$updateDataProtection();
170
+ this.currentUser.$updateGroupDataProtection();
126
171
  }
127
- (NymphUser as any).skipOnClone = true;
128
- this.Group = this.nymph.addEntityClass(NymphGroup);
129
172
 
173
+ // Set up access control hooks when Nymph is called.
174
+ this.nymph.config.debugInfo('tilmeld', 'Initializing access controls.');
130
175
  this.initAccessControl();
131
- if (this.request != null && !this.skipAuthenticate) {
132
- this.authenticate();
133
- }
134
- this.skipAuthenticate = false;
135
176
  }
136
177
 
137
178
  /**
@@ -143,11 +184,17 @@ export default class Tilmeld implements TilmeldInterface {
143
184
  * @param ability The ability.
144
185
  * @returns Whether the user has the given ability.
145
186
  */
146
- public gatekeeper(ability?: string): boolean {
147
- if (this.currentUser == null) {
187
+ public gatekeeper(ability?: string) {
188
+ if (this.currentUser == null || this.gatekeeperCache == null) {
148
189
  return false;
149
190
  }
150
- return this.currentUser.$gatekeeper(ability);
191
+ if (ability == null) {
192
+ return true;
193
+ }
194
+ return (
195
+ (ability in this.gatekeeperCache && !!this.gatekeeperCache[ability]) ||
196
+ !!this.currentUser.abilities?.includes('system/admin')
197
+ );
151
198
  }
152
199
 
153
200
  private initAccessControl() {
@@ -155,9 +202,9 @@ export default class Tilmeld implements TilmeldInterface {
155
202
  const handleQuery = function (
156
203
  nymph: Nymph,
157
204
  options: Options,
158
- selectors: FormattedSelector[]
205
+ selectors: FormattedSelector[],
159
206
  ) {
160
- const tilmeld = nymph.tilmeld as Tilmeld;
207
+ const tilmeld = enforceTilmeld(nymph);
161
208
  if (
162
209
  !options ||
163
210
  !('skipAc' in options) ||
@@ -166,7 +213,9 @@ export default class Tilmeld implements TilmeldInterface {
166
213
  ) {
167
214
  if (options?.source === 'client') {
168
215
  if (
169
- !tilmeld.gatekeeper('tilmeld/admin') && // The user is not an admin.
216
+ (!tilmeld.currentUser ||
217
+ (!tilmeld.currentUser.abilities?.includes('tilmeld/admin') &&
218
+ !tilmeld.currentUser.abilities?.includes('system/admin'))) && // The user is not an admin.
170
219
  ((!tilmeld.config.enableUserSearch &&
171
220
  options.class?.factorySync() instanceof User) || // The use is searching for a user and searching is disabled.
172
221
  (!tilmeld.config.enableGroupSearch &&
@@ -195,9 +244,9 @@ export default class Tilmeld implements TilmeldInterface {
195
244
  // Filter entities being deleted for user permissions.
196
245
  const checkPermissionsDelete = async function (
197
246
  nymph: Nymph,
198
- entity: EntityInterface
247
+ entity: EntityInterface,
199
248
  ) {
200
- const tilmeld = nymph.tilmeld as Tilmeld;
249
+ const tilmeld = enforceTilmeld(nymph);
201
250
  if (
202
251
  typeof entity.$tilmeldDeleteSkipAC === 'function' &&
203
252
  entity.$tilmeldDeleteSkipAC()
@@ -215,11 +264,11 @@ export default class Tilmeld implements TilmeldInterface {
215
264
  const checkPermissionsDeleteByID = async function (
216
265
  nymph: Nymph,
217
266
  guid: string,
218
- className?: string
267
+ className?: string,
219
268
  ) {
220
- const entity = nymph.driver.getEntitySync(
269
+ const entity = await nymph.getEntity(
221
270
  { class: nymph.getEntityClass(className ?? 'Entity') },
222
- { type: '&', guid: guid }
271
+ { type: '&', guid: guid },
223
272
  );
224
273
  if (entity != null) {
225
274
  checkPermissionsDelete(nymph, entity);
@@ -231,9 +280,9 @@ export default class Tilmeld implements TilmeldInterface {
231
280
  // disallowed changes to AC properties.
232
281
  const checkPermissionsSaveAndFilterAcChanges = async function (
233
282
  nymph: Nymph,
234
- entity: EntityInterface & AccessControlData
283
+ entity: EntityInterface & AccessControlData,
235
284
  ) {
236
- const tilmeld = nymph.tilmeld as Tilmeld;
285
+ const tilmeld = enforceTilmeld(nymph);
237
286
  if (!entity) {
238
287
  return;
239
288
  }
@@ -249,35 +298,9 @@ export default class Tilmeld implements TilmeldInterface {
249
298
  // allowing a change to ac properties.
250
299
 
251
300
  const originalAc = entity.$getOriginalAcValues();
252
- const newAc: AccessControlData = {
253
- user: entity.user ?? undefined,
254
- group: entity.group ?? undefined,
255
- acUser: entity.acUser ?? undefined,
256
- acGroup: entity.acGroup ?? undefined,
257
- acOther: entity.acOther ?? undefined,
258
- acRead: entity.acRead ?? undefined,
259
- acWrite: entity.acWrite ?? undefined,
260
- acFull: entity.acFull ?? undefined,
261
- };
262
-
263
- const setAcProperties = (acValues: { [k: string]: any }) => {
264
- for (let name in acValues) {
265
- const value = acValues[name];
266
- if (value == null) {
267
- delete entity[name];
268
- } else {
269
- entity[name] = value;
270
- }
271
- }
272
- };
301
+ const newAc = entity.$getCurrentAcValues();
273
302
 
274
- // Restore original AC properties and check permissions.
275
- setAcProperties(originalAc);
276
-
277
- function areAcPropertiesChanged(
278
- a: AccessControlData,
279
- b: AccessControlData
280
- ) {
303
+ function areAcPropertiesChanged(a: ACProperties, b: ACProperties) {
281
304
  for (const name of ['user', 'group'] as ('user' | 'group')[]) {
282
305
  const aVal = a[name];
283
306
  const bVal = b[name];
@@ -286,7 +309,7 @@ export default class Tilmeld implements TilmeldInterface {
286
309
  } else if (aVal == null || bVal == null) {
287
310
  return true;
288
311
  }
289
- if (!aVal.$is(bVal)) {
312
+ if (aVal !== bVal) {
290
313
  return true;
291
314
  }
292
315
  }
@@ -321,12 +344,7 @@ export default class Tilmeld implements TilmeldInterface {
321
344
  if (aVal.length !== bVal.length) {
322
345
  return true;
323
346
  }
324
- if (
325
- xor(
326
- aVal.map((entity) => entity.guid),
327
- bVal.map((entity) => entity.guid)
328
- ).length
329
- ) {
347
+ if (xor(aVal, bVal).length) {
330
348
  return true;
331
349
  }
332
350
  }
@@ -335,41 +353,42 @@ export default class Tilmeld implements TilmeldInterface {
335
353
 
336
354
  if (areAcPropertiesChanged(originalAc, newAc)) {
337
355
  if (
338
- !tilmeld.checkPermissions(entity, TilmeldAccessLevels.FULL_ACCESS)
356
+ !tilmeld.checkPermissions(
357
+ entity,
358
+ TilmeldAccessLevels.FULL_ACCESS,
359
+ undefined,
360
+ originalAc,
361
+ )
339
362
  ) {
340
363
  // Only allow changes to AC properties if the user has full access.
341
364
  throw new AccessControlError(
342
- 'No permission to change access control properties.'
365
+ 'No permission to change access control properties.',
343
366
  );
344
367
  }
345
368
 
346
369
  if (
347
- ((originalAc.user && !originalAc.user.$is(newAc.user)) ||
348
- (!originalAc.user &&
349
- newAc.user &&
350
- !newAc.user.$is(tilmeld.currentUser))) &&
351
- !tilmeld.gatekeeper('tilmeld/admin')
370
+ (originalAc.user != null && originalAc.user !== newAc.user) ||
371
+ (originalAc.user == null &&
372
+ newAc.user != null &&
373
+ newAc.user !== tilmeld.currentUser?.guid)
352
374
  ) {
353
375
  throw new AccessControlError(
354
- 'No permission to assign to another user.'
376
+ 'No permission to assign to another user.',
355
377
  );
356
378
  }
357
379
 
358
380
  if (
359
- newAc.group &&
360
- !newAc.group.$is(originalAc.group) &&
381
+ newAc.group != null &&
382
+ newAc.group !== originalAc.group &&
361
383
  !(
362
- newAc.group.$is(tilmeld.currentUser?.group) ||
363
- newAc.group.$inArray(tilmeld.currentUser?.groups ?? [])
364
- ) &&
365
- !tilmeld.gatekeeper('tilmeld/admin')
384
+ newAc.group === tilmeld.currentUser?.$getGid() ||
385
+ (tilmeld.currentUser?.$getGids() ?? []).includes(newAc.group)
386
+ )
366
387
  ) {
367
388
  throw new AccessControlError(
368
- 'No permission to assign to another group.'
389
+ 'No permission to assign to another group.',
369
390
  );
370
391
  }
371
-
372
- setAcProperties(newAc);
373
392
  }
374
393
  }
375
394
 
@@ -398,9 +417,9 @@ export default class Tilmeld implements TilmeldInterface {
398
417
  */
399
418
  const addAccess = async function (
400
419
  nymph: Nymph,
401
- entity: EntityInterface & AccessControlData
420
+ entity: EntityInterface & AccessControlData,
402
421
  ) {
403
- const tilmeld = nymph.tilmeld as Tilmeld;
422
+ const tilmeld = enforceTilmeld(nymph);
404
423
  const user = tilmeld.currentUser;
405
424
  if (
406
425
  user != null &&
@@ -411,12 +430,11 @@ export default class Tilmeld implements TilmeldInterface {
411
430
  if (entity.user == null) {
412
431
  entity.user = user;
413
432
  }
414
- if (
415
- entity.group == null &&
416
- user.group != null &&
417
- user.group.guid != null
418
- ) {
419
- entity.group = user.group;
433
+ if (entity.group == null) {
434
+ const group = user.group;
435
+ if (group != null && group.guid != null) {
436
+ entity.group = group;
437
+ }
420
438
  }
421
439
  if (entity.acUser == null) {
422
440
  entity.acUser = TilmeldAccessLevels.FULL_ACCESS;
@@ -442,7 +460,7 @@ export default class Tilmeld implements TilmeldInterface {
442
460
 
443
461
  const validate = async function (
444
462
  _nymph: Nymph,
445
- entity: EntityInterface & AccessControlData
463
+ entity: EntityInterface & AccessControlData,
446
464
  ) {
447
465
  if (!(entity instanceof User) && !(entity instanceof Group)) {
448
466
  const ownershipAcPropertyValidator = (prop: any) => {
@@ -452,7 +470,7 @@ export default class Tilmeld implements TilmeldInterface {
452
470
  prop > TilmeldAccessLevels.FULL_ACCESS
453
471
  ) {
454
472
  throw new AccessControlError(
455
- 'Invalid access control property: ' + prop
473
+ 'Invalid access control property: ' + prop,
456
474
  );
457
475
  }
458
476
  };
@@ -460,13 +478,13 @@ export default class Tilmeld implements TilmeldInterface {
460
478
  const accessAcPropertyValidator = (prop: any) => {
461
479
  if (!Array.isArray(prop)) {
462
480
  throw new AccessControlError(
463
- 'Invalid access control property: ' + prop
481
+ 'Invalid access control property: ' + prop,
464
482
  );
465
483
  }
466
484
  prop.forEach((value) => {
467
485
  if (!(value instanceof User || value instanceof Group)) {
468
486
  throw new AccessControlError(
469
- 'Invalid access control property: ' + prop
487
+ 'Invalid access control property: ' + prop,
470
488
  );
471
489
  }
472
490
  });
@@ -475,7 +493,7 @@ export default class Tilmeld implements TilmeldInterface {
475
493
  if ('user' in entity) {
476
494
  if (!(entity.user instanceof User)) {
477
495
  throw new AccessControlError(
478
- 'Invalid access control property: user'
496
+ 'Invalid access control property: user',
479
497
  );
480
498
  }
481
499
  }
@@ -483,7 +501,7 @@ export default class Tilmeld implements TilmeldInterface {
483
501
  if ('group' in entity) {
484
502
  if (!(entity.group instanceof Group)) {
485
503
  throw new AccessControlError(
486
- 'Invalid access control property: group'
504
+ 'Invalid access control property: group',
487
505
  );
488
506
  }
489
507
  }
@@ -528,18 +546,10 @@ export default class Tilmeld implements TilmeldInterface {
528
546
  */
529
547
  public addAccessControlSelectors(
530
548
  options: Options,
531
- selectors: FormattedSelector[]
549
+ selectors: FormattedSelector[],
532
550
  ) {
533
551
  const user = this.currentUser;
534
552
 
535
- if (
536
- user != null &&
537
- (user.abilities?.indexOf('system/admin') ?? -1) !== -1
538
- ) {
539
- // The user is a system admin, so they can see everything.
540
- return;
541
- }
542
-
543
553
  if (options == null) {
544
554
  throw new Error('No options in argument.');
545
555
  } else if (
@@ -579,33 +589,23 @@ export default class Tilmeld implements TilmeldInterface {
579
589
  ];
580
590
  const groupRefs: FormattedSelector['ref'] = [];
581
591
  const acRefs: FormattedSelector['ref'] = [];
582
- if (user.group != null && user.group.guid != null) {
592
+ const gid = user.$getGid();
593
+ if (gid != null) {
583
594
  // It belongs to the user's primary group.
584
- groupRefs.push(['group', user.group.guid]);
595
+ groupRefs.push(['group', gid]);
585
596
  // User's primary group is listed in acRead, acWrite, or acFull.
586
- acRefs.push(['acRead', user.group.guid]);
587
- acRefs.push(['acWrite', user.group.guid]);
588
- acRefs.push(['acFull', user.group.guid]);
589
- }
590
- for (let curSecondaryGroup of user.groups ?? []) {
591
- if (curSecondaryGroup != null && curSecondaryGroup.guid != null) {
592
- // It belongs to the user's secondary group.
593
- groupRefs.push(['group', curSecondaryGroup.guid]);
594
- // User's secondary group is listed in acRead, acWrite, or acFull.
595
- acRefs.push(['acRead', curSecondaryGroup.guid]);
596
- acRefs.push(['acWrite', curSecondaryGroup.guid]);
597
- acRefs.push(['acFull', curSecondaryGroup.guid]);
598
- }
597
+ acRefs.push(['acRead', gid]);
598
+ acRefs.push(['acWrite', gid]);
599
+ acRefs.push(['acFull', gid]);
599
600
  }
600
- for (let curDescendantGroup of user.$getDescendantGroupsSync()) {
601
- if (curDescendantGroup != null && curDescendantGroup.guid != null) {
602
- // It belongs to the user's secondary group.
603
- groupRefs.push(['group', curDescendantGroup.guid]);
604
- // User's secondary group is listed in acRead, acWrite, or acFull.
605
- acRefs.push(['acRead', curDescendantGroup.guid]);
606
- acRefs.push(['acWrite', curDescendantGroup.guid]);
607
- acRefs.push(['acFull', curDescendantGroup.guid]);
608
- }
601
+ const gids = user.$getGids();
602
+ for (let curGid of gids ?? []) {
603
+ // It belongs to the user's secondary group.
604
+ groupRefs.push(['group', curGid]);
605
+ // User's secondary group is listed in acRead, acWrite, or acFull.
606
+ acRefs.push(['acRead', curGid]);
607
+ acRefs.push(['acWrite', curGid]);
608
+ acRefs.push(['acFull', curGid]);
609
609
  }
610
610
  // All the group refs.
611
611
  if (groupRefs.length) {
@@ -671,7 +671,7 @@ export default class Tilmeld implements TilmeldInterface {
671
671
  * - FULL_ACCESS, the user has read, write, and delete access to the entity,
672
672
  * as well as being able to manage its access controls and ownership.
673
673
  *
674
- * These properties defaults to:
674
+ * These properties default to:
675
675
  *
676
676
  * - acUser = TilmeldAccessLevels.FULL_ACCESS
677
677
  * - acGroup = TilmeldAccessLevels.READ_ACCESS
@@ -686,9 +686,7 @@ export default class Tilmeld implements TilmeldInterface {
686
686
  * The following conditions will result in different checks, which determine
687
687
  * whether the check passes:
688
688
  *
689
- * - The user has the "system/admin" ability. (Always true.)
690
689
  * - It is a user or group. (True for READ_ACCESS or Tilmeld admins.)
691
- * - The entity has no "user" and no "group". (Always true.)
692
690
  * - No user is logged in. (Check other AC.)
693
691
  * - The entity is the user. (Always true.)
694
692
  * - It is the user's primary group. (True for READ_ACCESS.)
@@ -706,16 +704,17 @@ export default class Tilmeld implements TilmeldInterface {
706
704
  * @param entity The entity to check.
707
705
  * @param type The lowest level of permission to consider a pass.
708
706
  * @param user The user to check permissions for. If null, uses the current user. If false, checks for public access.
707
+ * @param acProperties The acProperties to use instead of getting them from the entity.
709
708
  * @returns Whether the current user has at least `type` permission for the entity.
710
709
  */
711
710
  public checkPermissions(
712
- entity: EntityInterface & AccessControlData,
711
+ entity: EntityInterface,
713
712
  type: TilmeldAccessLevels = TilmeldAccessLevels.READ_ACCESS,
714
- user?: (User & UserData) | false
713
+ user?: (User & UserData) | false,
714
+ acProperties?: ACProperties,
715
715
  ) {
716
- // Only works for entities.
717
- if (typeof entity !== 'object' || typeof entity.$is !== 'function') {
718
- return false;
716
+ if (!acProperties) {
717
+ acProperties = entity.$getCurrentAcValues() as ACProperties;
719
718
  }
720
719
 
721
720
  let userOrNull: (User & UserData) | null = null;
@@ -729,30 +728,22 @@ export default class Tilmeld implements TilmeldInterface {
729
728
  userOrEmpty = user;
730
729
  }
731
730
 
732
- if (userOrEmpty.$gatekeeper('system/admin')) {
733
- return true;
734
- }
735
-
736
731
  // Users and groups are always readable. Editable by Tilmeld admins.
737
732
  if (
738
733
  (entity instanceof User || entity instanceof Group) &&
739
734
  (type === TilmeldAccessLevels.READ_ACCESS ||
740
- userOrEmpty.$gatekeeper('tilmeld/admin'))
735
+ userOrEmpty.abilities?.includes('tilmeld/admin') ||
736
+ userOrEmpty.abilities?.includes('system/admin'))
741
737
  ) {
742
738
  return true;
743
739
  }
744
740
 
745
- // Entities with no owners are always editable.
746
- if (entity.user == null && entity.group == null) {
747
- return true;
748
- }
749
-
750
741
  // Load access control, since we need it now...
751
- const acUser = entity.acUser ?? TilmeldAccessLevels.FULL_ACCESS;
752
- const acGroup = entity.acGroup ?? TilmeldAccessLevels.READ_ACCESS;
753
- const acOther = entity.acOther ?? TilmeldAccessLevels.NO_ACCESS;
742
+ const acUser = acProperties.acUser ?? TilmeldAccessLevels.FULL_ACCESS;
743
+ const acGroup = acProperties.acGroup ?? TilmeldAccessLevels.READ_ACCESS;
744
+ const acOther = acProperties.acOther ?? TilmeldAccessLevels.NO_ACCESS;
754
745
 
755
- if (userOrNull === null) {
746
+ if (userOrNull == null) {
756
747
  return acOther >= type;
757
748
  }
758
749
 
@@ -762,45 +753,41 @@ export default class Tilmeld implements TilmeldInterface {
762
753
  }
763
754
 
764
755
  // Check if the entity is the user's group. Always readable.
756
+ const gid = userOrEmpty.$getGid();
765
757
  if (
766
- userOrEmpty.group != null &&
767
- typeof userOrEmpty.group.$is === 'function' &&
768
- userOrEmpty.group.$is(entity) &&
758
+ gid != null &&
759
+ entity.guid === gid &&
769
760
  type === TilmeldAccessLevels.READ_ACCESS
770
761
  ) {
771
762
  return true;
772
763
  }
773
764
 
774
765
  // Calculate all the groups the user belongs to.
775
- let allGroups = userOrEmpty.group == null ? [] : [userOrEmpty.group];
776
- allGroups = allGroups.concat(userOrEmpty.groups ?? []);
777
- allGroups = allGroups.concat(userOrEmpty.$getDescendantGroupsSync());
766
+ let allGids = gid == null ? [] : [gid];
767
+ allGids = allGids.concat(userOrEmpty.$getGids() ?? []);
778
768
 
779
769
  // Check access ac properties.
780
770
  const checks = [
781
771
  {
782
772
  type: TilmeldAccessLevels.FULL_ACCESS,
783
- array: [...(entity.acFull ?? [])],
773
+ array: [...(acProperties.acFull ?? [])],
784
774
  },
785
775
  {
786
776
  type: TilmeldAccessLevels.WRITE_ACCESS,
787
- array: [...(entity.acWrite ?? [])],
777
+ array: [...(acProperties.acWrite ?? [])],
788
778
  },
789
779
  {
790
780
  type: TilmeldAccessLevels.READ_ACCESS,
791
- array: [...(entity.acRead ?? [])],
781
+ array: [...(acProperties.acRead ?? [])],
792
782
  },
793
783
  ];
794
784
  for (let curCheck of checks) {
795
785
  if (type <= curCheck.type) {
796
- if (userOrEmpty.$inArray(curCheck.array)) {
786
+ if (curCheck.array.includes(userOrEmpty.guid)) {
797
787
  return true;
798
788
  }
799
- for (let curGroup of allGroups) {
800
- if (
801
- typeof curGroup.$inArray === 'function' &&
802
- curGroup.$inArray(curCheck.array)
803
- ) {
789
+ for (let curGid of allGids) {
790
+ if (curCheck.array.includes(curGid)) {
804
791
  return true;
805
792
  }
806
793
  }
@@ -808,18 +795,10 @@ export default class Tilmeld implements TilmeldInterface {
808
795
  }
809
796
 
810
797
  // Check ownership ac properties.
811
- if (
812
- entity.user != null &&
813
- typeof entity.user.$is === 'function' &&
814
- entity.user.$is(userOrNull)
815
- ) {
798
+ if (acProperties.user != null && acProperties.user === userOrNull.guid) {
816
799
  return acUser >= type;
817
800
  }
818
- if (
819
- entity.group != null &&
820
- typeof entity.group.$inArray === 'function' &&
821
- entity.group.$inArray(allGroups)
822
- ) {
801
+ if (acProperties.group != null && allGids.includes(acProperties.group)) {
823
802
  return acGroup >= type;
824
803
  }
825
804
  return acOther >= type;
@@ -849,10 +828,10 @@ export default class Tilmeld implements TilmeldInterface {
849
828
  * @param user The user to check permissions for. If null, uses the current user. If false, checks for public access.
850
829
  * @returns Whether the current user has at least `type` permission for the UID.
851
830
  */
852
- public checkClientUIDPermissions(
831
+ public async checkClientUIDPermissions(
853
832
  name: string,
854
833
  type: TilmeldAccessLevels = TilmeldAccessLevels.READ_ACCESS,
855
- user?: (User & UserData) | false
834
+ user?: (User & UserData) | false,
856
835
  ) {
857
836
  let userOrEmpty: User & UserData = this.User.factorySync();
858
837
  // Calculate the user.
@@ -862,30 +841,30 @@ export default class Tilmeld implements TilmeldInterface {
862
841
  userOrEmpty = user;
863
842
  }
864
843
 
865
- if (userOrEmpty.$gatekeeper('system/admin')) {
844
+ if (userOrEmpty.abilities?.includes('system/admin')) {
866
845
  return true;
867
846
  }
868
847
 
869
848
  if (type === TilmeldAccessLevels.FULL_ACCESS) {
870
849
  return (
871
- this.config.clientSetabledUIDs.indexOf(name) !== -1 ||
872
- userOrEmpty.$gatekeeper(`uid/set/${name}`)
850
+ this.config.clientSetabledUIDs.includes(name) ||
851
+ (await userOrEmpty.$gatekeeper(`uid/set/${name}`))
873
852
  );
874
853
  } else if (type === TilmeldAccessLevels.WRITE_ACCESS) {
875
854
  return (
876
- this.config.clientEnabledUIDs.indexOf(name) !== -1 ||
877
- this.config.clientSetabledUIDs.indexOf(name) !== -1 ||
878
- userOrEmpty.$gatekeeper(`uid/new/${name}`) ||
879
- userOrEmpty.$gatekeeper(`uid/set/${name}`)
855
+ this.config.clientEnabledUIDs.includes(name) ||
856
+ this.config.clientSetabledUIDs.includes(name) ||
857
+ (await userOrEmpty.$gatekeeper(`uid/new/${name}`)) ||
858
+ (await userOrEmpty.$gatekeeper(`uid/set/${name}`))
880
859
  );
881
860
  } else if (type === TilmeldAccessLevels.READ_ACCESS) {
882
861
  return (
883
- this.config.clientReadableUIDs.indexOf(name) !== -1 ||
884
- this.config.clientEnabledUIDs.indexOf(name) !== -1 ||
885
- this.config.clientSetabledUIDs.indexOf(name) !== -1 ||
886
- userOrEmpty.$gatekeeper(`uid/get/${name}`) ||
887
- userOrEmpty.$gatekeeper(`uid/new/${name}`) ||
888
- userOrEmpty.$gatekeeper(`uid/set/${name}`)
862
+ this.config.clientReadableUIDs.includes(name) ||
863
+ this.config.clientEnabledUIDs.includes(name) ||
864
+ this.config.clientSetabledUIDs.includes(name) ||
865
+ (await userOrEmpty.$gatekeeper(`uid/get/${name}`)) ||
866
+ (await userOrEmpty.$gatekeeper(`uid/new/${name}`)) ||
867
+ (await userOrEmpty.$gatekeeper(`uid/set/${name}`))
889
868
  );
890
869
  }
891
870
 
@@ -897,20 +876,13 @@ export default class Tilmeld implements TilmeldInterface {
897
876
  *
898
877
  * @param user The user.
899
878
  */
900
- public fillSession(user: User & UserData) {
901
- // Read groups right now, since gatekeeper needs them, so
902
- // $udpateDataProtection will fail to read them (since it runs gatekeeper).
903
- const _group = user.group;
904
- const _groups = user.groups;
879
+ public async fillSession(user: User & UserData) {
905
880
  this.currentUser = user;
881
+ this.gatekeeperCache = await user.$getGatekeeperCache();
882
+
906
883
  // Now update the data protection on the user and all the groups.
907
884
  this.currentUser.$updateDataProtection();
908
- if (this.currentUser.group != null) {
909
- this.currentUser.group.$updateDataProtection();
910
- }
911
- for (let group of this.currentUser.groups ?? []) {
912
- group.$updateDataProtection();
913
- }
885
+ this.currentUser.$updateGroupDataProtection();
914
886
  }
915
887
 
916
888
  /**
@@ -919,14 +891,10 @@ export default class Tilmeld implements TilmeldInterface {
919
891
  public clearSession() {
920
892
  const user = this.currentUser;
921
893
  this.currentUser = null;
894
+ this.gatekeeperCache = null;
922
895
  if (user) {
923
896
  user.$updateDataProtection();
924
- if (user.group != null) {
925
- user.group.$updateDataProtection();
926
- }
927
- for (let group of user.groups ?? []) {
928
- group.$updateDataProtection();
929
- }
897
+ user.$updateGroupDataProtection();
930
898
  }
931
899
  }
932
900
 
@@ -937,6 +905,7 @@ export default class Tilmeld implements TilmeldInterface {
937
905
  * @returns The user on success, null on failure.
938
906
  */
939
907
  public async extractToken(token: string) {
908
+ this.nymph.config.debugInfo('tilmeld', 'Extracting and validating token.');
940
909
  const extract = this.config.jwtExtract(this.config, token);
941
910
  if (extract == null) {
942
911
  return null;
@@ -948,9 +917,9 @@ export default class Tilmeld implements TilmeldInterface {
948
917
  {
949
918
  type: '&',
950
919
  guid: guid,
951
- }
920
+ },
952
921
  );
953
- if (!user || !user.guid || !user.enabled) {
922
+ if (!user || !user.guid) {
954
923
  return null;
955
924
  }
956
925
 
@@ -958,40 +927,73 @@ export default class Tilmeld implements TilmeldInterface {
958
927
  }
959
928
 
960
929
  /**
961
- * Check for a TILMELDAUTH token, and, if set, authenticate from it.
930
+ * Check for TILMELDAUTH and TILMELDSWITCH tokens, and, if set, authenticate
931
+ * from it/them.
932
+ *
933
+ * You can also call this function after setting `response.locals.user` to the
934
+ * user you want to authenticate. You *should* check for `user.enabled` before
935
+ * setting this variable, unless you explicitly want to log in as a disabled
936
+ * user. (The user must be an instance of the User class for this Tilmeld
937
+ * instance.)
938
+ *
939
+ * This function will set `response.locals.user` to the logged in user on
940
+ * successful authentication.
962
941
  *
963
942
  * @param skipXsrfToken Skip the XSRF token check.
943
+ * @param skipRenew Skip the token renewal step, even if the token is close to expiration.
964
944
  * @returns True if a user was authenticated, false on any failure.
965
945
  */
966
- public authenticate(skipXsrfToken = false) {
946
+ public async authenticate(skipXsrfToken = false, skipRenew = false) {
967
947
  if (this.request == null) {
968
948
  return false;
969
949
  }
970
950
 
951
+ if (
952
+ this.response &&
953
+ this.response.locals.user &&
954
+ this.response.locals.user instanceof this.User
955
+ ) {
956
+ // The user has already been authenticated through some other means.
957
+ const user = this.response.locals.user;
958
+ await this.fillSession(user);
959
+ return true;
960
+ }
961
+
971
962
  const cookies = this.request.cookies ?? {};
972
963
 
973
964
  // If a client does't support cookies, they can use the X-TILMELDAUTH header
974
965
  // to provide the auth token.
975
966
  let fromAuthHeader = false;
976
967
  let authToken: string;
968
+ let switchToken: string | null;
977
969
  if (this.request.header('x-tilmeldauth') != null) {
978
970
  fromAuthHeader = true;
979
971
  authToken = this.request.header('x-tilmeldauth') as string;
972
+ switchToken =
973
+ (this.request.header('x-tilmeldswitch') as string) ||
974
+ 'TILMELDSWITCH' in cookies
975
+ ? cookies.TILMELDSWITCH
976
+ : null;
980
977
  } else if ('TILMELDAUTH' in cookies) {
981
978
  fromAuthHeader = false;
982
979
  authToken = cookies.TILMELDAUTH;
980
+ switchToken = 'TILMELDSWITCH' in cookies ? cookies.TILMELDSWITCH : null;
983
981
  } else {
984
982
  return false;
985
983
  }
986
984
 
987
- let extract: { guid: string; expire: Date } | null;
985
+ let authExtract: { guid: string; issued: Date; expire: Date } | null;
986
+ let switchExtract: { guid: string; expire: Date } | null;
988
987
  if (
989
988
  skipXsrfToken ||
990
989
  this.request.originalUrl.startsWith(this.config.setupPath)
991
990
  ) {
992
991
  // The request is for the setup app, or we were told to skip the XSRF
993
992
  // check, so don't check for the XSRF token.
994
- extract = this.config.jwtExtract(this.config, authToken);
993
+ authExtract = this.config.jwtExtract(this.config, authToken);
994
+ switchExtract = switchToken
995
+ ? this.config.jwtExtract(this.config, switchToken)
996
+ : null;
995
997
  } else {
996
998
  // The request is for something else, so check for a valid XSRF token,
997
999
  // unless the auth token is provided by a header (instead of a cookie).
@@ -1000,28 +1002,65 @@ export default class Tilmeld implements TilmeldInterface {
1000
1002
  return false;
1001
1003
  }
1002
1004
 
1003
- extract = this.config.jwtExtract(this.config, authToken, xsrfToken);
1005
+ authExtract = this.config.jwtExtract(this.config, authToken, xsrfToken);
1006
+ switchExtract = switchToken
1007
+ ? this.config.jwtExtract(this.config, switchToken)
1008
+ : null;
1004
1009
  }
1005
1010
 
1006
- if (extract == null) {
1007
- this.logout();
1011
+ if (authExtract == null) {
1012
+ await this.logout();
1008
1013
  return false;
1009
1014
  }
1010
- const { guid, expire } = extract;
1015
+ const { guid, issued, expire } = authExtract;
1016
+ const { guid: switchGuid, expire: switchExpire } = switchExtract || {
1017
+ guid: null,
1018
+ expire: null,
1019
+ };
1011
1020
 
1012
- const user = this.User.factorySync(guid);
1013
- if (user.guid == null || !user.enabled) {
1014
- this.logout();
1021
+ const user = await this.User.factory(guid);
1022
+ if (
1023
+ user.guid == null ||
1024
+ !user.enabled ||
1025
+ expire.getTime() <= Date.now() ||
1026
+ issued.getTime() > Date.now() ||
1027
+ (user.revokeTokenDate != null && issued.getTime() <= user.revokeTokenDate)
1028
+ ) {
1029
+ await this.logout();
1015
1030
  return false;
1016
1031
  }
1017
1032
 
1018
- if (expire.valueOf() < Date.now() + this.config.jwtRenew * 1000) {
1033
+ if (switchGuid && switchExpire && switchExpire.getTime() > Date.now()) {
1034
+ // Load the switch user.
1035
+ const switchUser = await this.User.factory(switchGuid);
1036
+ if (switchUser.guid == null) {
1037
+ await this.logoutSwitch();
1038
+ } else {
1039
+ await this.fillSession(switchUser);
1040
+
1041
+ if (this.response) {
1042
+ this.response.locals.user = switchUser;
1043
+ }
1044
+
1045
+ return true;
1046
+ }
1047
+ }
1048
+
1049
+ if (
1050
+ !skipRenew &&
1051
+ expire.getTime() < Date.now() + this.config.jwtRenew * 1000
1052
+ ) {
1019
1053
  // If the user is less than renew time from needing a new token, give them
1020
1054
  // a new one.
1021
- this.login(user, fromAuthHeader);
1055
+ await this.login(user, fromAuthHeader);
1022
1056
  } else {
1023
- this.fillSession(user);
1057
+ await this.fillSession(user);
1058
+ }
1059
+
1060
+ if (this.response) {
1061
+ this.response.locals.user = user;
1024
1062
  }
1063
+
1025
1064
  return true;
1026
1065
  }
1027
1066
 
@@ -1029,27 +1068,87 @@ export default class Tilmeld implements TilmeldInterface {
1029
1068
  * Logs the given user into the system.
1030
1069
  *
1031
1070
  * @param user The user.
1032
- * @param sendAuthHeader When true, a custom header with the auth token will be sent.
1071
+ * @param sendAuthHeader Send the auth token as a custom header.
1072
+ * @param sendCookie Send the auth token as a cookie.
1033
1073
  * @returns True on success, false on failure.
1034
1074
  */
1035
- public login(user: User & UserData, sendAuthHeader: boolean) {
1075
+ public async login(
1076
+ user: User & UserData,
1077
+ sendAuthHeader: boolean,
1078
+ sendCookie = true,
1079
+ ) {
1036
1080
  if (user.guid != null && user.enabled) {
1037
- if (this.response && !this.response.headersSent) {
1038
- const token = this.config.jwtBuilder(this.config, user);
1039
- const appUrl = new URL(this.config.appUrl);
1040
- this.response.cookie('TILMELDAUTH', token, {
1041
- domain: this.config.cookieDomain,
1042
- path: this.config.cookiePath,
1043
- maxAge: this.config.jwtExpire * 1000,
1044
- secure: appUrl.protocol === 'https',
1045
- httpOnly: false, // Allow JS access (for CSRF protection).
1046
- sameSite: appUrl.protocol === 'https' ? 'lax' : 'strict',
1047
- });
1048
- if (sendAuthHeader) {
1049
- this.response.set('X-TILMELDAUTH', token);
1081
+ if (this.response) {
1082
+ if (!this.response.headersSent) {
1083
+ const token = this.config.jwtBuilder(this.config, user);
1084
+
1085
+ if (sendCookie) {
1086
+ const appUrl = new URL(this.config.appUrl);
1087
+ this.response.cookie('TILMELDAUTH', token, {
1088
+ domain: this.config.cookieDomain,
1089
+ path: this.config.cookiePath,
1090
+ maxAge: this.config.jwtExpire * 1000,
1091
+ secure: appUrl.protocol === 'https:',
1092
+ httpOnly: false, // Allow JS access (for CSRF protection).
1093
+ sameSite: appUrl.protocol === 'https:' ? 'lax' : 'strict',
1094
+ });
1095
+ }
1096
+
1097
+ if (sendAuthHeader) {
1098
+ this.response.set('X-TILMELDAUTH', token);
1099
+ }
1100
+ }
1101
+
1102
+ this.response.locals.user = user;
1103
+ }
1104
+
1105
+ await this.fillSession(user);
1106
+ return true;
1107
+ }
1108
+ return false;
1109
+ }
1110
+
1111
+ /**
1112
+ * Adds a switch auth token for the given user.
1113
+ *
1114
+ * This effectively logs the current user in to the system as the given user.
1115
+ *
1116
+ * @param user The user.
1117
+ * @param sendAuthHeader Send the auth token as a custom header.
1118
+ * @param sendCookie Send the auth token as a cookie.
1119
+ * @returns True on success, false on failure.
1120
+ */
1121
+ public async loginSwitch(
1122
+ user: User & UserData,
1123
+ sendAuthHeader: boolean,
1124
+ sendCookie = true,
1125
+ ) {
1126
+ if (user.guid != null) {
1127
+ if (this.response) {
1128
+ if (!this.response.headersSent) {
1129
+ const token = this.config.jwtBuilder(this.config, user, true);
1130
+
1131
+ if (sendCookie) {
1132
+ const appUrl = new URL(this.config.appUrl);
1133
+ this.response.cookie('TILMELDSWITCH', token, {
1134
+ domain: this.config.cookieDomain,
1135
+ path: this.config.cookiePath,
1136
+ maxAge: this.config.jwtSwitchExpire * 1000,
1137
+ secure: appUrl.protocol === 'https:',
1138
+ httpOnly: false, // Allow JS access (for PubSub auth token).
1139
+ sameSite: appUrl.protocol === 'https:' ? 'lax' : 'strict',
1140
+ });
1141
+ }
1142
+
1143
+ if (sendAuthHeader) {
1144
+ this.response.set('X-TILMELDSWITCH', token);
1145
+ }
1050
1146
  }
1147
+
1148
+ this.response.locals.user = user;
1051
1149
  }
1052
- this.fillSession(user);
1150
+
1151
+ await this.fillSession(user);
1053
1152
  return true;
1054
1153
  }
1055
1154
  return false;
@@ -1057,15 +1156,50 @@ export default class Tilmeld implements TilmeldInterface {
1057
1156
 
1058
1157
  /**
1059
1158
  * Logs the current user out of the system.
1159
+ *
1160
+ * @param clearCookie Clear the auth cookie. (Also send a header.)
1060
1161
  */
1061
- public logout() {
1162
+ public async logout(clearCookie = true) {
1163
+ if (this.request && !this.alreadyLoggedOutSwitch) {
1164
+ const cookies = this.request.cookies ?? {};
1165
+
1166
+ if ('TILMELDSWITCH' in cookies) {
1167
+ this.alreadyLoggedOutSwitch = true;
1168
+ return await this.logoutSwitch(clearCookie);
1169
+ }
1170
+ }
1171
+
1062
1172
  this.clearSession();
1063
- if (this.response && !this.response.headersSent) {
1064
- this.response.clearCookie('TILMELDAUTH', {
1065
- domain: this.config.cookieDomain,
1066
- path: this.config.cookiePath,
1067
- });
1068
- this.response.set('X-TILMELDAUTH', '');
1173
+ if (this.response) {
1174
+ if (clearCookie && !this.response.headersSent) {
1175
+ this.response.clearCookie('TILMELDAUTH', {
1176
+ domain: this.config.cookieDomain,
1177
+ path: this.config.cookiePath,
1178
+ });
1179
+ this.response.set('X-TILMELDAUTH', '');
1180
+ }
1181
+ this.response.locals.user = null;
1182
+ }
1183
+ }
1184
+
1185
+ /**
1186
+ * Clears the switch user out of the system.
1187
+ *
1188
+ * @param clearCookie Clear the auth cookie. (Also send a header.)
1189
+ */
1190
+ public async logoutSwitch(clearCookie = true) {
1191
+ this.clearSession();
1192
+ if (this.response) {
1193
+ if (clearCookie && !this.response.headersSent) {
1194
+ this.response.clearCookie('TILMELDSWITCH', {
1195
+ domain: this.config.cookieDomain,
1196
+ path: this.config.cookiePath,
1197
+ });
1198
+ this.response.set('X-TILMELDSWITCH', '');
1199
+ }
1200
+ this.response.locals.user = null;
1201
+
1202
+ await this.authenticate(false, true);
1069
1203
  }
1070
1204
  }
1071
1205