@nymphjs/tilmeld 1.0.0-beta.6 → 1.0.0-beta.61

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 +284 -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 +119 -8
  7. package/dist/Group.js +230 -97
  8. package/dist/Group.js.map +1 -1
  9. package/dist/Group.test.js +3 -2
  10. package/dist/Group.test.js.map +1 -1
  11. package/dist/Tilmeld.d.ts +237 -9
  12. package/dist/Tilmeld.js +559 -195
  13. package/dist/Tilmeld.js.map +1 -1
  14. package/dist/User.d.ts +333 -14
  15. package/dist/User.js +797 -204
  16. package/dist/User.js.map +1 -1
  17. package/dist/User.test.js +3 -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 +3 -3
  43. package/src/Group.ts +194 -143
  44. package/src/Tilmeld.ts +384 -249
  45. package/src/User.test.ts +3 -3
  46. package/src/User.ts +876 -266
  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 +1 -1
  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,10 +90,29 @@ 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
  /**
86
114
  * This is used internally by Nymph. Don't call it yourself.
115
+ *
87
116
  * @returns A clone of this instance.
88
117
  */
89
118
  public clone() {
@@ -91,7 +120,7 @@ export default class Tilmeld implements TilmeldInterface {
91
120
  tilmeld.request = this.request;
92
121
  tilmeld.response = this.response;
93
122
  tilmeld.currentUser = this.currentUser;
94
- tilmeld.skipAuthenticate = true;
123
+ tilmeld.gatekeeperCache = this.gatekeeperCache;
95
124
 
96
125
  return tilmeld;
97
126
  }
@@ -105,32 +134,45 @@ export default class Tilmeld implements TilmeldInterface {
105
134
  */
106
135
  public init(nymph: Nymph) {
107
136
  this.nymph = nymph;
108
- const tilmeld = this;
109
137
 
110
- // Set up access control hooks when Nymph is called.
111
138
  if (this.nymph.driver == null) {
112
139
  throw new Error("Tilmeld can't be configured before Nymph.");
113
140
  }
114
141
 
115
142
  // Configure the classes.
116
- class NymphUser extends User {
117
- static nymph: Nymph = nymph;
118
- 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);
119
151
  }
120
- (NymphUser as any).skipOnClone = true;
121
- this.User = this.nymph.addEntityClass(NymphUser);
122
- class NymphGroup extends Group {
123
- static nymph: Nymph = nymph;
124
- 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();
125
171
  }
126
- (NymphUser as any).skipOnClone = true;
127
- this.Group = this.nymph.addEntityClass(NymphGroup);
128
172
 
173
+ // Set up access control hooks when Nymph is called.
174
+ this.nymph.config.debugLog('tilmeld', 'Initializing access controls.');
129
175
  this.initAccessControl();
130
- if (this.request != null && !this.skipAuthenticate) {
131
- this.authenticate();
132
- }
133
- this.skipAuthenticate = false;
134
176
  }
135
177
 
136
178
  /**
@@ -142,11 +184,17 @@ export default class Tilmeld implements TilmeldInterface {
142
184
  * @param ability The ability.
143
185
  * @returns Whether the user has the given ability.
144
186
  */
145
- public gatekeeper(ability?: string): boolean {
146
- if (this.currentUser == null) {
187
+ public gatekeeper(ability?: string) {
188
+ if (this.currentUser == null || this.gatekeeperCache == null) {
147
189
  return false;
148
190
  }
149
- 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
+ );
150
198
  }
151
199
 
152
200
  private initAccessControl() {
@@ -154,9 +202,9 @@ export default class Tilmeld implements TilmeldInterface {
154
202
  const handleQuery = function (
155
203
  nymph: Nymph,
156
204
  options: Options,
157
- selectors: FormattedSelector[]
205
+ selectors: FormattedSelector[],
158
206
  ) {
159
- const tilmeld = nymph.tilmeld as Tilmeld;
207
+ const tilmeld = enforceTilmeld(nymph);
160
208
  if (
161
209
  !options ||
162
210
  !('skipAc' in options) ||
@@ -165,7 +213,9 @@ export default class Tilmeld implements TilmeldInterface {
165
213
  ) {
166
214
  if (options?.source === 'client') {
167
215
  if (
168
- !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.
169
219
  ((!tilmeld.config.enableUserSearch &&
170
220
  options.class?.factorySync() instanceof User) || // The use is searching for a user and searching is disabled.
171
221
  (!tilmeld.config.enableGroupSearch &&
@@ -194,9 +244,9 @@ export default class Tilmeld implements TilmeldInterface {
194
244
  // Filter entities being deleted for user permissions.
195
245
  const checkPermissionsDelete = async function (
196
246
  nymph: Nymph,
197
- entity: EntityInterface
247
+ entity: EntityInterface,
198
248
  ) {
199
- const tilmeld = nymph.tilmeld as Tilmeld;
249
+ const tilmeld = enforceTilmeld(nymph);
200
250
  if (
201
251
  typeof entity.$tilmeldDeleteSkipAC === 'function' &&
202
252
  entity.$tilmeldDeleteSkipAC()
@@ -214,11 +264,11 @@ export default class Tilmeld implements TilmeldInterface {
214
264
  const checkPermissionsDeleteByID = async function (
215
265
  nymph: Nymph,
216
266
  guid: string,
217
- className?: string
267
+ className?: string,
218
268
  ) {
219
- const entity = nymph.driver.getEntitySync(
269
+ const entity = await nymph.getEntity(
220
270
  { class: nymph.getEntityClass(className ?? 'Entity') },
221
- { type: '&', guid: guid }
271
+ { type: '&', guid: guid },
222
272
  );
223
273
  if (entity != null) {
224
274
  checkPermissionsDelete(nymph, entity);
@@ -230,9 +280,9 @@ export default class Tilmeld implements TilmeldInterface {
230
280
  // disallowed changes to AC properties.
231
281
  const checkPermissionsSaveAndFilterAcChanges = async function (
232
282
  nymph: Nymph,
233
- entity: EntityInterface & AccessControlData
283
+ entity: EntityInterface & AccessControlData,
234
284
  ) {
235
- const tilmeld = nymph.tilmeld as Tilmeld;
285
+ const tilmeld = enforceTilmeld(nymph);
236
286
  if (!entity) {
237
287
  return;
238
288
  }
@@ -248,35 +298,9 @@ export default class Tilmeld implements TilmeldInterface {
248
298
  // allowing a change to ac properties.
249
299
 
250
300
  const originalAc = entity.$getOriginalAcValues();
251
- const newAc: AccessControlData = {
252
- user: entity.user ?? undefined,
253
- group: entity.group ?? undefined,
254
- acUser: entity.acUser ?? undefined,
255
- acGroup: entity.acGroup ?? undefined,
256
- acOther: entity.acOther ?? undefined,
257
- acRead: entity.acRead ?? undefined,
258
- acWrite: entity.acWrite ?? undefined,
259
- acFull: entity.acFull ?? undefined,
260
- };
261
-
262
- const setAcProperties = (acValues: { [k: string]: any }) => {
263
- for (let name in acValues) {
264
- const value = acValues[name];
265
- if (value == null) {
266
- delete entity[name];
267
- } else {
268
- entity[name] = value;
269
- }
270
- }
271
- };
272
-
273
- // Restore original AC properties and check permissions.
274
- setAcProperties(originalAc);
301
+ const newAc = entity.$getCurrentAcValues();
275
302
 
276
- function areAcPropertiesChanged(
277
- a: AccessControlData,
278
- b: AccessControlData
279
- ) {
303
+ function areAcPropertiesChanged(a: ACProperties, b: ACProperties) {
280
304
  for (const name of ['user', 'group'] as ('user' | 'group')[]) {
281
305
  const aVal = a[name];
282
306
  const bVal = b[name];
@@ -285,7 +309,7 @@ export default class Tilmeld implements TilmeldInterface {
285
309
  } else if (aVal == null || bVal == null) {
286
310
  return true;
287
311
  }
288
- if (!aVal.$is(bVal)) {
312
+ if (aVal !== bVal) {
289
313
  return true;
290
314
  }
291
315
  }
@@ -320,12 +344,7 @@ export default class Tilmeld implements TilmeldInterface {
320
344
  if (aVal.length !== bVal.length) {
321
345
  return true;
322
346
  }
323
- if (
324
- xor(
325
- aVal.map((entity) => entity.guid),
326
- bVal.map((entity) => entity.guid)
327
- ).length
328
- ) {
347
+ if (xor(aVal, bVal).length) {
329
348
  return true;
330
349
  }
331
350
  }
@@ -334,41 +353,42 @@ export default class Tilmeld implements TilmeldInterface {
334
353
 
335
354
  if (areAcPropertiesChanged(originalAc, newAc)) {
336
355
  if (
337
- !tilmeld.checkPermissions(entity, TilmeldAccessLevels.FULL_ACCESS)
356
+ !tilmeld.checkPermissions(
357
+ entity,
358
+ TilmeldAccessLevels.FULL_ACCESS,
359
+ undefined,
360
+ originalAc,
361
+ )
338
362
  ) {
339
363
  // Only allow changes to AC properties if the user has full access.
340
364
  throw new AccessControlError(
341
- 'No permission to change access control properties.'
365
+ 'No permission to change access control properties.',
342
366
  );
343
367
  }
344
368
 
345
369
  if (
346
- ((originalAc.user && !originalAc.user.$is(newAc.user)) ||
347
- (!originalAc.user &&
348
- newAc.user &&
349
- !newAc.user.$is(tilmeld.currentUser))) &&
350
- !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)
351
374
  ) {
352
375
  throw new AccessControlError(
353
- 'No permission to assign to another user.'
376
+ 'No permission to assign to another user.',
354
377
  );
355
378
  }
356
379
 
357
380
  if (
358
- newAc.group &&
359
- !newAc.group.$is(originalAc.group) &&
381
+ newAc.group != null &&
382
+ newAc.group !== originalAc.group &&
360
383
  !(
361
- newAc.group.$is(tilmeld.currentUser?.group) ||
362
- newAc.group.$inArray(tilmeld.currentUser?.groups ?? [])
363
- ) &&
364
- !tilmeld.gatekeeper('tilmeld/admin')
384
+ newAc.group === tilmeld.currentUser?.$getGid() ||
385
+ (tilmeld.currentUser?.$getGids() ?? []).includes(newAc.group)
386
+ )
365
387
  ) {
366
388
  throw new AccessControlError(
367
- 'No permission to assign to another group.'
389
+ 'No permission to assign to another group.',
368
390
  );
369
391
  }
370
-
371
- setAcProperties(newAc);
372
392
  }
373
393
  }
374
394
 
@@ -397,9 +417,9 @@ export default class Tilmeld implements TilmeldInterface {
397
417
  */
398
418
  const addAccess = async function (
399
419
  nymph: Nymph,
400
- entity: EntityInterface & AccessControlData
420
+ entity: EntityInterface & AccessControlData,
401
421
  ) {
402
- const tilmeld = nymph.tilmeld as Tilmeld;
422
+ const tilmeld = enforceTilmeld(nymph);
403
423
  const user = tilmeld.currentUser;
404
424
  if (
405
425
  user != null &&
@@ -410,12 +430,11 @@ export default class Tilmeld implements TilmeldInterface {
410
430
  if (entity.user == null) {
411
431
  entity.user = user;
412
432
  }
413
- if (
414
- entity.group == null &&
415
- user.group != null &&
416
- user.group.guid != null
417
- ) {
418
- 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
+ }
419
438
  }
420
439
  if (entity.acUser == null) {
421
440
  entity.acUser = TilmeldAccessLevels.FULL_ACCESS;
@@ -441,7 +460,7 @@ export default class Tilmeld implements TilmeldInterface {
441
460
 
442
461
  const validate = async function (
443
462
  _nymph: Nymph,
444
- entity: EntityInterface & AccessControlData
463
+ entity: EntityInterface & AccessControlData,
445
464
  ) {
446
465
  if (!(entity instanceof User) && !(entity instanceof Group)) {
447
466
  const ownershipAcPropertyValidator = (prop: any) => {
@@ -451,7 +470,7 @@ export default class Tilmeld implements TilmeldInterface {
451
470
  prop > TilmeldAccessLevels.FULL_ACCESS
452
471
  ) {
453
472
  throw new AccessControlError(
454
- 'Invalid access control property: ' + prop
473
+ 'Invalid access control property: ' + prop,
455
474
  );
456
475
  }
457
476
  };
@@ -459,13 +478,13 @@ export default class Tilmeld implements TilmeldInterface {
459
478
  const accessAcPropertyValidator = (prop: any) => {
460
479
  if (!Array.isArray(prop)) {
461
480
  throw new AccessControlError(
462
- 'Invalid access control property: ' + prop
481
+ 'Invalid access control property: ' + prop,
463
482
  );
464
483
  }
465
484
  prop.forEach((value) => {
466
485
  if (!(value instanceof User || value instanceof Group)) {
467
486
  throw new AccessControlError(
468
- 'Invalid access control property: ' + prop
487
+ 'Invalid access control property: ' + prop,
469
488
  );
470
489
  }
471
490
  });
@@ -474,7 +493,7 @@ export default class Tilmeld implements TilmeldInterface {
474
493
  if ('user' in entity) {
475
494
  if (!(entity.user instanceof User)) {
476
495
  throw new AccessControlError(
477
- 'Invalid access control property: user'
496
+ 'Invalid access control property: user',
478
497
  );
479
498
  }
480
499
  }
@@ -482,7 +501,7 @@ export default class Tilmeld implements TilmeldInterface {
482
501
  if ('group' in entity) {
483
502
  if (!(entity.group instanceof Group)) {
484
503
  throw new AccessControlError(
485
- 'Invalid access control property: group'
504
+ 'Invalid access control property: group',
486
505
  );
487
506
  }
488
507
  }
@@ -527,18 +546,10 @@ export default class Tilmeld implements TilmeldInterface {
527
546
  */
528
547
  public addAccessControlSelectors(
529
548
  options: Options,
530
- selectors: FormattedSelector[]
549
+ selectors: FormattedSelector[],
531
550
  ) {
532
551
  const user = this.currentUser;
533
552
 
534
- if (
535
- user != null &&
536
- (user.abilities?.indexOf('system/admin') ?? -1) !== -1
537
- ) {
538
- // The user is a system admin, so they can see everything.
539
- return;
540
- }
541
-
542
553
  if (options == null) {
543
554
  throw new Error('No options in argument.');
544
555
  } else if (
@@ -578,33 +589,23 @@ export default class Tilmeld implements TilmeldInterface {
578
589
  ];
579
590
  const groupRefs: FormattedSelector['ref'] = [];
580
591
  const acRefs: FormattedSelector['ref'] = [];
581
- if (user.group != null && user.group.guid != null) {
592
+ const gid = user.$getGid();
593
+ if (gid != null) {
582
594
  // It belongs to the user's primary group.
583
- groupRefs.push(['group', user.group.guid]);
595
+ groupRefs.push(['group', gid]);
584
596
  // User's primary group is listed in acRead, acWrite, or acFull.
585
- acRefs.push(['acRead', user.group.guid]);
586
- acRefs.push(['acWrite', user.group.guid]);
587
- acRefs.push(['acFull', user.group.guid]);
588
- }
589
- for (let curSecondaryGroup of user.groups ?? []) {
590
- if (curSecondaryGroup != null && curSecondaryGroup.guid != null) {
591
- // It belongs to the user's secondary group.
592
- groupRefs.push(['group', curSecondaryGroup.guid]);
593
- // User's secondary group is listed in acRead, acWrite, or acFull.
594
- acRefs.push(['acRead', curSecondaryGroup.guid]);
595
- acRefs.push(['acWrite', curSecondaryGroup.guid]);
596
- acRefs.push(['acFull', curSecondaryGroup.guid]);
597
- }
597
+ acRefs.push(['acRead', gid]);
598
+ acRefs.push(['acWrite', gid]);
599
+ acRefs.push(['acFull', gid]);
598
600
  }
599
- for (let curDescendantGroup of user.$getDescendantGroupsSync()) {
600
- if (curDescendantGroup != null && curDescendantGroup.guid != null) {
601
- // It belongs to the user's secondary group.
602
- groupRefs.push(['group', curDescendantGroup.guid]);
603
- // User's secondary group is listed in acRead, acWrite, or acFull.
604
- acRefs.push(['acRead', curDescendantGroup.guid]);
605
- acRefs.push(['acWrite', curDescendantGroup.guid]);
606
- acRefs.push(['acFull', curDescendantGroup.guid]);
607
- }
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]);
608
609
  }
609
610
  // All the group refs.
610
611
  if (groupRefs.length) {
@@ -670,7 +671,7 @@ export default class Tilmeld implements TilmeldInterface {
670
671
  * - FULL_ACCESS, the user has read, write, and delete access to the entity,
671
672
  * as well as being able to manage its access controls and ownership.
672
673
  *
673
- * These properties defaults to:
674
+ * These properties default to:
674
675
  *
675
676
  * - acUser = TilmeldAccessLevels.FULL_ACCESS
676
677
  * - acGroup = TilmeldAccessLevels.READ_ACCESS
@@ -685,9 +686,7 @@ export default class Tilmeld implements TilmeldInterface {
685
686
  * The following conditions will result in different checks, which determine
686
687
  * whether the check passes:
687
688
  *
688
- * - The user has the "system/admin" ability. (Always true.)
689
689
  * - It is a user or group. (True for READ_ACCESS or Tilmeld admins.)
690
- * - The entity has no "user" and no "group". (Always true.)
691
690
  * - No user is logged in. (Check other AC.)
692
691
  * - The entity is the user. (Always true.)
693
692
  * - It is the user's primary group. (True for READ_ACCESS.)
@@ -705,16 +704,17 @@ export default class Tilmeld implements TilmeldInterface {
705
704
  * @param entity The entity to check.
706
705
  * @param type The lowest level of permission to consider a pass.
707
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.
708
708
  * @returns Whether the current user has at least `type` permission for the entity.
709
709
  */
710
710
  public checkPermissions(
711
- entity: EntityInterface & AccessControlData,
711
+ entity: EntityInterface,
712
712
  type: TilmeldAccessLevels = TilmeldAccessLevels.READ_ACCESS,
713
- user?: (User & UserData) | false
713
+ user?: (User & UserData) | false,
714
+ acProperties?: ACProperties,
714
715
  ) {
715
- // Only works for entities.
716
- if (typeof entity !== 'object' || typeof entity.$is !== 'function') {
717
- return false;
716
+ if (!acProperties) {
717
+ acProperties = entity.$getCurrentAcValues() as ACProperties;
718
718
  }
719
719
 
720
720
  let userOrNull: (User & UserData) | null = null;
@@ -728,30 +728,22 @@ export default class Tilmeld implements TilmeldInterface {
728
728
  userOrEmpty = user;
729
729
  }
730
730
 
731
- if (userOrEmpty.$gatekeeper('system/admin')) {
732
- return true;
733
- }
734
-
735
731
  // Users and groups are always readable. Editable by Tilmeld admins.
736
732
  if (
737
733
  (entity instanceof User || entity instanceof Group) &&
738
734
  (type === TilmeldAccessLevels.READ_ACCESS ||
739
- userOrEmpty.$gatekeeper('tilmeld/admin'))
735
+ userOrEmpty.abilities?.includes('tilmeld/admin') ||
736
+ userOrEmpty.abilities?.includes('system/admin'))
740
737
  ) {
741
738
  return true;
742
739
  }
743
740
 
744
- // Entities with no owners are always editable.
745
- if (entity.user == null && entity.group == null) {
746
- return true;
747
- }
748
-
749
741
  // Load access control, since we need it now...
750
- const acUser = entity.acUser ?? TilmeldAccessLevels.FULL_ACCESS;
751
- const acGroup = entity.acGroup ?? TilmeldAccessLevels.READ_ACCESS;
752
- 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;
753
745
 
754
- if (userOrNull === null) {
746
+ if (userOrNull == null) {
755
747
  return acOther >= type;
756
748
  }
757
749
 
@@ -761,45 +753,41 @@ export default class Tilmeld implements TilmeldInterface {
761
753
  }
762
754
 
763
755
  // Check if the entity is the user's group. Always readable.
756
+ const gid = userOrEmpty.$getGid();
764
757
  if (
765
- userOrEmpty.group != null &&
766
- typeof userOrEmpty.group.$is === 'function' &&
767
- userOrEmpty.group.$is(entity) &&
758
+ gid != null &&
759
+ entity.guid === gid &&
768
760
  type === TilmeldAccessLevels.READ_ACCESS
769
761
  ) {
770
762
  return true;
771
763
  }
772
764
 
773
765
  // Calculate all the groups the user belongs to.
774
- let allGroups = userOrEmpty.group == null ? [] : [userOrEmpty.group];
775
- allGroups = allGroups.concat(userOrEmpty.groups ?? []);
776
- allGroups = allGroups.concat(userOrEmpty.$getDescendantGroupsSync());
766
+ let allGids = gid == null ? [] : [gid];
767
+ allGids = allGids.concat(userOrEmpty.$getGids() ?? []);
777
768
 
778
769
  // Check access ac properties.
779
770
  const checks = [
780
771
  {
781
772
  type: TilmeldAccessLevels.FULL_ACCESS,
782
- array: [...(entity.acFull ?? [])],
773
+ array: [...(acProperties.acFull ?? [])],
783
774
  },
784
775
  {
785
776
  type: TilmeldAccessLevels.WRITE_ACCESS,
786
- array: [...(entity.acWrite ?? [])],
777
+ array: [...(acProperties.acWrite ?? [])],
787
778
  },
788
779
  {
789
780
  type: TilmeldAccessLevels.READ_ACCESS,
790
- array: [...(entity.acRead ?? [])],
781
+ array: [...(acProperties.acRead ?? [])],
791
782
  },
792
783
  ];
793
784
  for (let curCheck of checks) {
794
785
  if (type <= curCheck.type) {
795
- if (userOrEmpty.$inArray(curCheck.array)) {
786
+ if (curCheck.array.includes(userOrEmpty.guid)) {
796
787
  return true;
797
788
  }
798
- for (let curGroup of allGroups) {
799
- if (
800
- typeof curGroup.$inArray === 'function' &&
801
- curGroup.$inArray(curCheck.array)
802
- ) {
789
+ for (let curGid of allGids) {
790
+ if (curCheck.array.includes(curGid)) {
803
791
  return true;
804
792
  }
805
793
  }
@@ -807,18 +795,10 @@ export default class Tilmeld implements TilmeldInterface {
807
795
  }
808
796
 
809
797
  // Check ownership ac properties.
810
- if (
811
- entity.user != null &&
812
- typeof entity.user.$is === 'function' &&
813
- entity.user.$is(userOrNull)
814
- ) {
798
+ if (acProperties.user != null && acProperties.user === userOrNull.guid) {
815
799
  return acUser >= type;
816
800
  }
817
- if (
818
- entity.group != null &&
819
- typeof entity.group.$inArray === 'function' &&
820
- entity.group.$inArray(allGroups)
821
- ) {
801
+ if (acProperties.group != null && allGids.includes(acProperties.group)) {
822
802
  return acGroup >= type;
823
803
  }
824
804
  return acOther >= type;
@@ -848,10 +828,10 @@ export default class Tilmeld implements TilmeldInterface {
848
828
  * @param user The user to check permissions for. If null, uses the current user. If false, checks for public access.
849
829
  * @returns Whether the current user has at least `type` permission for the UID.
850
830
  */
851
- public checkClientUIDPermissions(
831
+ public async checkClientUIDPermissions(
852
832
  name: string,
853
833
  type: TilmeldAccessLevels = TilmeldAccessLevels.READ_ACCESS,
854
- user?: (User & UserData) | false
834
+ user?: (User & UserData) | false,
855
835
  ) {
856
836
  let userOrEmpty: User & UserData = this.User.factorySync();
857
837
  // Calculate the user.
@@ -861,30 +841,30 @@ export default class Tilmeld implements TilmeldInterface {
861
841
  userOrEmpty = user;
862
842
  }
863
843
 
864
- if (userOrEmpty.$gatekeeper('system/admin')) {
844
+ if (userOrEmpty.abilities?.includes('system/admin')) {
865
845
  return true;
866
846
  }
867
847
 
868
848
  if (type === TilmeldAccessLevels.FULL_ACCESS) {
869
849
  return (
870
- this.config.clientSetabledUIDs.indexOf(name) !== -1 ||
871
- userOrEmpty.$gatekeeper(`uid/set/${name}`)
850
+ this.config.clientSetabledUIDs.includes(name) ||
851
+ (await userOrEmpty.$gatekeeper(`uid/set/${name}`))
872
852
  );
873
853
  } else if (type === TilmeldAccessLevels.WRITE_ACCESS) {
874
854
  return (
875
- this.config.clientEnabledUIDs.indexOf(name) !== -1 ||
876
- this.config.clientSetabledUIDs.indexOf(name) !== -1 ||
877
- userOrEmpty.$gatekeeper(`uid/new/${name}`) ||
878
- 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}`))
879
859
  );
880
860
  } else if (type === TilmeldAccessLevels.READ_ACCESS) {
881
861
  return (
882
- this.config.clientReadableUIDs.indexOf(name) !== -1 ||
883
- this.config.clientEnabledUIDs.indexOf(name) !== -1 ||
884
- this.config.clientSetabledUIDs.indexOf(name) !== -1 ||
885
- userOrEmpty.$gatekeeper(`uid/get/${name}`) ||
886
- userOrEmpty.$gatekeeper(`uid/new/${name}`) ||
887
- 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}`))
888
868
  );
889
869
  }
890
870
 
@@ -896,20 +876,13 @@ export default class Tilmeld implements TilmeldInterface {
896
876
  *
897
877
  * @param user The user.
898
878
  */
899
- public fillSession(user: User & UserData) {
900
- // Read groups right now, since gatekeeper needs them, so
901
- // $udpateDataProtection will fail to read them (since it runs gatekeeper).
902
- const _group = user.group;
903
- const _groups = user.groups;
879
+ public async fillSession(user: User & UserData) {
904
880
  this.currentUser = user;
881
+ this.gatekeeperCache = await user.$getGatekeeperCache();
882
+
905
883
  // Now update the data protection on the user and all the groups.
906
884
  this.currentUser.$updateDataProtection();
907
- if (this.currentUser.group != null) {
908
- this.currentUser.group.$updateDataProtection();
909
- }
910
- for (let group of this.currentUser.groups ?? []) {
911
- group.$updateDataProtection();
912
- }
885
+ this.currentUser.$updateGroupDataProtection();
913
886
  }
914
887
 
915
888
  /**
@@ -918,14 +891,10 @@ export default class Tilmeld implements TilmeldInterface {
918
891
  public clearSession() {
919
892
  const user = this.currentUser;
920
893
  this.currentUser = null;
894
+ this.gatekeeperCache = null;
921
895
  if (user) {
922
896
  user.$updateDataProtection();
923
- if (user.group != null) {
924
- user.group.$updateDataProtection();
925
- }
926
- for (let group of user.groups ?? []) {
927
- group.$updateDataProtection();
928
- }
897
+ user.$updateGroupDataProtection();
929
898
  }
930
899
  }
931
900
 
@@ -936,6 +905,7 @@ export default class Tilmeld implements TilmeldInterface {
936
905
  * @returns The user on success, null on failure.
937
906
  */
938
907
  public async extractToken(token: string) {
908
+ this.nymph.config.debugLog('tilmeld', 'Extracting and validating token.');
939
909
  const extract = this.config.jwtExtract(this.config, token);
940
910
  if (extract == null) {
941
911
  return null;
@@ -947,9 +917,9 @@ export default class Tilmeld implements TilmeldInterface {
947
917
  {
948
918
  type: '&',
949
919
  guid: guid,
950
- }
920
+ },
951
921
  );
952
- if (!user || !user.guid || !user.enabled) {
922
+ if (!user || !user.guid) {
953
923
  return null;
954
924
  }
955
925
 
@@ -957,40 +927,73 @@ export default class Tilmeld implements TilmeldInterface {
957
927
  }
958
928
 
959
929
  /**
960
- * 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.
961
941
  *
962
942
  * @param skipXsrfToken Skip the XSRF token check.
943
+ * @param skipRenew Skip the token renewal step, even if the token is close to expiration.
963
944
  * @returns True if a user was authenticated, false on any failure.
964
945
  */
965
- public authenticate(skipXsrfToken = false) {
946
+ public async authenticate(skipXsrfToken = false, skipRenew = false) {
966
947
  if (this.request == null) {
967
948
  return false;
968
949
  }
969
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
+
970
962
  const cookies = this.request.cookies ?? {};
971
963
 
972
964
  // If a client does't support cookies, they can use the X-TILMELDAUTH header
973
965
  // to provide the auth token.
974
966
  let fromAuthHeader = false;
975
967
  let authToken: string;
968
+ let switchToken: string | null;
976
969
  if (this.request.header('x-tilmeldauth') != null) {
977
970
  fromAuthHeader = true;
978
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;
979
977
  } else if ('TILMELDAUTH' in cookies) {
980
978
  fromAuthHeader = false;
981
979
  authToken = cookies.TILMELDAUTH;
980
+ switchToken = 'TILMELDSWITCH' in cookies ? cookies.TILMELDSWITCH : null;
982
981
  } else {
983
982
  return false;
984
983
  }
985
984
 
986
- 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;
987
987
  if (
988
988
  skipXsrfToken ||
989
989
  this.request.originalUrl.startsWith(this.config.setupPath)
990
990
  ) {
991
991
  // The request is for the setup app, or we were told to skip the XSRF
992
992
  // check, so don't check for the XSRF token.
993
- 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;
994
997
  } else {
995
998
  // The request is for something else, so check for a valid XSRF token,
996
999
  // unless the auth token is provided by a header (instead of a cookie).
@@ -999,28 +1002,65 @@ export default class Tilmeld implements TilmeldInterface {
999
1002
  return false;
1000
1003
  }
1001
1004
 
1002
- 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;
1003
1009
  }
1004
1010
 
1005
- if (extract == null) {
1006
- this.logout();
1011
+ if (authExtract == null) {
1012
+ await this.logout();
1007
1013
  return false;
1008
1014
  }
1009
- const { guid, expire } = extract;
1015
+ const { guid, issued, expire } = authExtract;
1016
+ const { guid: switchGuid, expire: switchExpire } = switchExtract || {
1017
+ guid: null,
1018
+ expire: null,
1019
+ };
1010
1020
 
1011
- const user = this.User.factorySync(guid);
1012
- if (user.guid == null || !user.enabled) {
1013
- 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();
1014
1030
  return false;
1015
1031
  }
1016
1032
 
1017
- 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
+ ) {
1018
1053
  // If the user is less than renew time from needing a new token, give them
1019
1054
  // a new one.
1020
- this.login(user, fromAuthHeader);
1055
+ await this.login(user, fromAuthHeader);
1021
1056
  } else {
1022
- this.fillSession(user);
1057
+ await this.fillSession(user);
1058
+ }
1059
+
1060
+ if (this.response) {
1061
+ this.response.locals.user = user;
1023
1062
  }
1063
+
1024
1064
  return true;
1025
1065
  }
1026
1066
 
@@ -1028,27 +1068,87 @@ export default class Tilmeld implements TilmeldInterface {
1028
1068
  * Logs the given user into the system.
1029
1069
  *
1030
1070
  * @param user The user.
1031
- * @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.
1032
1073
  * @returns True on success, false on failure.
1033
1074
  */
1034
- public login(user: User & UserData, sendAuthHeader: boolean) {
1075
+ public async login(
1076
+ user: User & UserData,
1077
+ sendAuthHeader: boolean,
1078
+ sendCookie = true,
1079
+ ) {
1035
1080
  if (user.guid != null && user.enabled) {
1036
- if (this.response && !this.response.headersSent) {
1037
- const token = this.config.jwtBuilder(this.config, user);
1038
- const appUrl = new URL(this.config.appUrl);
1039
- this.response.cookie('TILMELDAUTH', token, {
1040
- domain: this.config.cookieDomain,
1041
- path: this.config.cookiePath,
1042
- maxAge: this.config.jwtExpire * 1000,
1043
- secure: appUrl.protocol === 'https',
1044
- httpOnly: false, // Allow JS access (for CSRF protection).
1045
- sameSite: appUrl.protocol === 'https' ? 'lax' : 'strict',
1046
- });
1047
- if (sendAuthHeader) {
1048
- 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
+ }
1049
1100
  }
1101
+
1102
+ this.response.locals.user = user;
1050
1103
  }
1051
- this.fillSession(user);
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
+ }
1146
+ }
1147
+
1148
+ this.response.locals.user = user;
1149
+ }
1150
+
1151
+ await this.fillSession(user);
1052
1152
  return true;
1053
1153
  }
1054
1154
  return false;
@@ -1056,15 +1156,50 @@ export default class Tilmeld implements TilmeldInterface {
1056
1156
 
1057
1157
  /**
1058
1158
  * Logs the current user out of the system.
1159
+ *
1160
+ * @param clearCookie Clear the auth cookie. (Also send a header.)
1059
1161
  */
1060
- 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
+
1061
1172
  this.clearSession();
1062
- if (this.response && !this.response.headersSent) {
1063
- this.response.clearCookie('TILMELDAUTH', {
1064
- domain: this.config.cookieDomain,
1065
- path: this.config.cookiePath,
1066
- });
1067
- 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);
1068
1203
  }
1069
1204
  }
1070
1205