@nymphjs/tilmeld 1.0.0-beta.12 → 1.0.0-beta.121

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 (93) hide show
  1. package/CHANGELOG.md +550 -0
  2. package/README.md +15 -2
  3. package/dist/AbleObject.d.ts +15 -0
  4. package/dist/AbleObject.js +18 -6
  5. package/dist/AbleObject.js.map +1 -1
  6. package/dist/Group.d.ts +182 -15
  7. package/dist/Group.js +571 -191
  8. package/dist/Group.js.map +1 -1
  9. package/dist/Group.testArtifacts.d.ts +3 -0
  10. package/dist/Group.testArtifacts.js +120 -0
  11. package/dist/Group.testArtifacts.js.map +1 -0
  12. package/dist/Tilmeld.d.ts +241 -12
  13. package/dist/Tilmeld.js +721 -273
  14. package/dist/Tilmeld.js.map +1 -1
  15. package/dist/Tilmeld.types.d.ts +5 -5
  16. package/dist/Tilmeld.types.js +1 -2
  17. package/dist/User.d.ts +396 -22
  18. package/dist/User.js +1386 -367
  19. package/dist/User.js.map +1 -1
  20. package/dist/User.testArtifacts.d.ts +3 -0
  21. package/dist/User.testArtifacts.js +175 -0
  22. package/dist/User.testArtifacts.js.map +1 -0
  23. package/dist/conf/defaults.d.ts +1 -1
  24. package/dist/conf/defaults.js +196 -141
  25. package/dist/conf/defaults.js.map +1 -1
  26. package/dist/conf/index.d.ts +355 -2
  27. package/dist/conf/index.js +2 -8
  28. package/dist/conf/index.js.map +1 -1
  29. package/dist/enforceTilmeld.d.ts +3 -0
  30. package/dist/enforceTilmeld.js +18 -0
  31. package/dist/enforceTilmeld.js.map +1 -0
  32. package/dist/errors/AccessControlError.d.ts +2 -1
  33. package/dist/errors/AccessControlError.js +3 -6
  34. package/dist/errors/AccessControlError.js.map +1 -1
  35. package/dist/errors/BadDataError.d.ts +2 -1
  36. package/dist/errors/BadDataError.js +3 -6
  37. package/dist/errors/BadDataError.js.map +1 -1
  38. package/dist/errors/BadEmailError.d.ts +2 -1
  39. package/dist/errors/BadEmailError.js +3 -6
  40. package/dist/errors/BadEmailError.js.map +1 -1
  41. package/dist/errors/BadUsernameError.d.ts +2 -1
  42. package/dist/errors/BadUsernameError.js +3 -6
  43. package/dist/errors/BadUsernameError.js.map +1 -1
  44. package/dist/errors/CouldNotChangeDefaultPrimaryGroupError.js +1 -5
  45. package/dist/errors/CouldNotChangeDefaultPrimaryGroupError.js.map +1 -1
  46. package/dist/errors/EmailChangeRateLimitExceededError.js +1 -5
  47. package/dist/errors/EmailChangeRateLimitExceededError.js.map +1 -1
  48. package/dist/errors/index.d.ts +6 -6
  49. package/dist/errors/index.js +6 -22
  50. package/dist/errors/index.js.map +1 -1
  51. package/dist/index.d.ts +11 -10
  52. package/dist/index.js +16 -35
  53. package/dist/index.js.map +1 -1
  54. package/dist/testArtifacts.d.ts +19 -0
  55. package/dist/testArtifacts.js +1173 -0
  56. package/dist/testArtifacts.js.map +1 -0
  57. package/dist/tilmeldJoiProps.d.ts +3 -3
  58. package/dist/tilmeldJoiProps.js +13 -18
  59. package/dist/tilmeldJoiProps.js.map +1 -1
  60. package/jest.config.js +31 -5
  61. package/package.json +41 -34
  62. package/src/AbleObject.ts +2 -2
  63. package/src/Group.testArtifacts.ts +159 -0
  64. package/src/Group.ts +684 -225
  65. package/src/Tilmeld.ts +547 -283
  66. package/src/Tilmeld.types.ts +5 -5
  67. package/src/User.testArtifacts.ts +226 -0
  68. package/src/User.ts +1635 -461
  69. package/src/conf/defaults.ts +172 -109
  70. package/src/conf/index.ts +370 -2
  71. package/src/enforceTilmeld.ts +20 -0
  72. package/src/errors/AccessControlError.ts +4 -2
  73. package/src/errors/BadDataError.ts +4 -2
  74. package/src/errors/BadEmailError.ts +4 -2
  75. package/src/errors/BadUsernameError.ts +4 -2
  76. package/src/errors/index.ts +6 -6
  77. package/src/index.ts +12 -10
  78. package/src/testArtifacts.ts +1979 -0
  79. package/src/tilmeldJoiProps.ts +8 -23
  80. package/tsconfig.json +6 -3
  81. package/typedoc.json +4 -0
  82. package/dist/Group.test.d.ts +0 -1
  83. package/dist/Group.test.js +0 -48
  84. package/dist/Group.test.js.map +0 -1
  85. package/dist/User.test.d.ts +0 -1
  86. package/dist/User.test.js +0 -51
  87. package/dist/User.test.js.map +0 -1
  88. package/dist/conf/d.d.ts +0 -55
  89. package/dist/conf/d.js +0 -3
  90. package/dist/conf/d.js.map +0 -1
  91. package/src/Group.test.ts +0 -58
  92. package/src/User.test.ts +0 -61
  93. package/src/conf/d.ts +0 -303
package/src/Tilmeld.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { URL } from 'node:url';
1
2
  import {
2
3
  Nymph,
3
4
  EntityInterface,
@@ -5,23 +6,26 @@ import {
5
6
  Options,
6
7
  TilmeldInterface,
7
8
  TilmeldAccessLevels,
9
+ TilmeldAccessRequest,
10
+ ACProperties,
8
11
  } from '@nymphjs/nymph';
9
- import { URL } from 'url';
10
12
  import { Request, Response } from 'express';
11
- import { xor } from 'lodash';
13
+ import { xor } from 'lodash-es';
14
+ import { splitn } from '@sciactive/splitn';
12
15
 
13
- import { Config, ConfigDefaults as defaults } from './conf';
14
- import { AccessControlError } from './errors';
15
- import Group from './Group';
16
- import User, { UserData } from './User';
17
- import { AccessControlData } from './Tilmeld.types';
16
+ import { Config, ConfigDefaults as defaults } from './conf/index.js';
17
+ import { AccessControlError } from './errors/index.js';
18
+ import Group, { GroupData } from './Group.js';
19
+ import User, { UserData } from './User.js';
20
+ import { AccessControlData } from './Tilmeld.types.js';
21
+ import { enforceTilmeld } from './enforceTilmeld.js';
18
22
 
19
23
  /**
20
24
  * A user and group system for Nymph.js.
21
25
  *
22
26
  * Written by Hunter Perrin for SciActive.
23
27
  *
24
- * @author Hunter Perrin <hperrin@gmail.com>
28
+ * @author Hunter Perrin <hperrin@port87.com>
25
29
  * @copyright SciActive Inc
26
30
  * @see http://nymph.io/
27
31
  */
@@ -69,9 +73,17 @@ export default class Tilmeld implements TilmeldInterface {
69
73
  public response: Response | null = null;
70
74
 
71
75
  /**
72
- * Skip the authenticate step of initialization.
76
+ * Used to avoid infinite loop.
73
77
  */
74
- private skipAuthenticate = false;
78
+ private alreadyLoggedOutSwitch = false;
79
+
80
+ /**
81
+ * Gatekeeper ability cache.
82
+ *
83
+ * Gatekeeper will cache the user's abilities that it calculates, so it can
84
+ * check faster if that user has been checked before.
85
+ */
86
+ private gatekeeperCache: { [k: string]: true } | null = null;
75
87
 
76
88
  /**
77
89
  * Create a new instance of Tilmeld.
@@ -80,6 +92,30 @@ export default class Tilmeld implements TilmeldInterface {
80
92
  */
81
93
  public constructor(config: Partial<Config> & { jwtSecret: string }) {
82
94
  this.config = { ...defaults, ...config };
95
+
96
+ if (
97
+ this.config.emailUsernames &&
98
+ (!this.config.userFields.includes('email') ||
99
+ !this.config.regFields.includes('email'))
100
+ ) {
101
+ throw new Error(
102
+ 'You must include email in userFields and regFields if you use emailUsernames.',
103
+ );
104
+ }
105
+
106
+ for (let field of this.config.regFields) {
107
+ if (!this.config.userFields.includes(field)) {
108
+ throw new Error(
109
+ `All fields in regFields must also be in userFields: ${field}`,
110
+ );
111
+ }
112
+ }
113
+
114
+ if (this.config.emailUsernames && this.config.domainSupport) {
115
+ throw new Error(
116
+ 'Email usernames and domain support cannot both be enabled.',
117
+ );
118
+ }
83
119
  }
84
120
 
85
121
  /**
@@ -92,7 +128,7 @@ export default class Tilmeld implements TilmeldInterface {
92
128
  tilmeld.request = this.request;
93
129
  tilmeld.response = this.response;
94
130
  tilmeld.currentUser = this.currentUser;
95
- tilmeld.skipAuthenticate = true;
131
+ tilmeld.gatekeeperCache = this.gatekeeperCache;
96
132
 
97
133
  return tilmeld;
98
134
  }
@@ -106,32 +142,47 @@ export default class Tilmeld implements TilmeldInterface {
106
142
  */
107
143
  public init(nymph: Nymph) {
108
144
  this.nymph = nymph;
109
- const tilmeld = this;
110
145
 
111
- // Set up access control hooks when Nymph is called.
112
146
  if (this.nymph.driver == null) {
113
147
  throw new Error("Tilmeld can't be configured before Nymph.");
114
148
  }
115
149
 
116
150
  // Configure the classes.
117
- class NymphUser extends User {
118
- static nymph: Nymph = nymph;
119
- static tilmeld: Tilmeld = tilmeld;
151
+ if (nymph.parent) {
152
+ // This is a cloned Nymph, it should have classes already.
153
+ this.User = this.nymph.getEntityClass(User);
154
+ this.Group = this.nymph.getEntityClass(Group);
155
+ } else {
156
+ // This is a new Nymph, add the classes.
157
+ this.User = this.nymph.addEntityClass(User);
158
+ this.Group = this.nymph.addEntityClass(Group);
120
159
  }
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;
160
+
161
+ // Clone the current user so its Nymph instance is correct.
162
+ if (this.currentUser) {
163
+ const currentUserGUID = this.currentUser.guid;
164
+ const currentUserCDate = this.currentUser.cdate;
165
+ const currentUserMDate = this.currentUser.mdate;
166
+ const currentUserTags = this.currentUser.tags;
167
+ const currentUserData = this.currentUser.$getData();
168
+ const currentUserSData = this.currentUser.$getSData();
169
+
170
+ const user = this.User.factorySync();
171
+ user.guid = currentUserGUID;
172
+ user.cdate = currentUserCDate;
173
+ user.mdate = currentUserMDate;
174
+ user.tags = currentUserTags;
175
+ user.$putData(currentUserData, currentUserSData);
176
+ user.$originalEmail = this.currentUser.$originalEmail;
177
+ user.$originalUsername = this.currentUser.$originalUsername;
178
+ this.currentUser = user;
179
+ this.currentUser.$updateDataProtection();
180
+ this.currentUser.$updateGroupDataProtection();
126
181
  }
127
- (NymphUser as any).skipOnClone = true;
128
- this.Group = this.nymph.addEntityClass(NymphGroup);
129
182
 
183
+ // Set up access control hooks when Nymph is called.
184
+ this.nymph.config.debugInfo('tilmeld', 'Initializing access controls.');
130
185
  this.initAccessControl();
131
- if (this.request != null && !this.skipAuthenticate) {
132
- this.authenticate();
133
- }
134
- this.skipAuthenticate = false;
135
186
  }
136
187
 
137
188
  /**
@@ -143,11 +194,17 @@ export default class Tilmeld implements TilmeldInterface {
143
194
  * @param ability The ability.
144
195
  * @returns Whether the user has the given ability.
145
196
  */
146
- public gatekeeper(ability?: string): boolean {
147
- if (this.currentUser == null) {
197
+ public gatekeeper(ability?: string) {
198
+ if (this.currentUser == null || this.gatekeeperCache == null) {
148
199
  return false;
149
200
  }
150
- return this.currentUser.$gatekeeper(ability);
201
+ if (ability == null) {
202
+ return true;
203
+ }
204
+ return (
205
+ (ability in this.gatekeeperCache && !!this.gatekeeperCache[ability]) ||
206
+ !!this.currentUser.abilities?.includes('system/admin')
207
+ );
151
208
  }
152
209
 
153
210
  private initAccessControl() {
@@ -155,9 +212,9 @@ export default class Tilmeld implements TilmeldInterface {
155
212
  const handleQuery = function (
156
213
  nymph: Nymph,
157
214
  options: Options,
158
- selectors: FormattedSelector[]
215
+ selectors: FormattedSelector[],
159
216
  ) {
160
- const tilmeld = nymph.tilmeld as Tilmeld;
217
+ const tilmeld = enforceTilmeld(nymph);
161
218
  if (
162
219
  !options ||
163
220
  !('skipAc' in options) ||
@@ -166,7 +223,9 @@ export default class Tilmeld implements TilmeldInterface {
166
223
  ) {
167
224
  if (options?.source === 'client') {
168
225
  if (
169
- !tilmeld.gatekeeper('tilmeld/admin') && // The user is not an admin.
226
+ (!tilmeld.currentUser ||
227
+ (!tilmeld.currentUser.abilities?.includes('tilmeld/admin') &&
228
+ !tilmeld.currentUser.abilities?.includes('system/admin'))) && // The user is not an admin.
170
229
  ((!tilmeld.config.enableUserSearch &&
171
230
  options.class?.factorySync() instanceof User) || // The use is searching for a user and searching is disabled.
172
231
  (!tilmeld.config.enableGroupSearch &&
@@ -195,9 +254,9 @@ export default class Tilmeld implements TilmeldInterface {
195
254
  // Filter entities being deleted for user permissions.
196
255
  const checkPermissionsDelete = async function (
197
256
  nymph: Nymph,
198
- entity: EntityInterface
257
+ entity: EntityInterface,
199
258
  ) {
200
- const tilmeld = nymph.tilmeld as Tilmeld;
259
+ const tilmeld = enforceTilmeld(nymph);
201
260
  if (
202
261
  typeof entity.$tilmeldDeleteSkipAC === 'function' &&
203
262
  entity.$tilmeldDeleteSkipAC()
@@ -215,11 +274,14 @@ export default class Tilmeld implements TilmeldInterface {
215
274
  const checkPermissionsDeleteByID = async function (
216
275
  nymph: Nymph,
217
276
  guid: string,
218
- className?: string
277
+ className?: string,
219
278
  ) {
220
- const entity = nymph.driver.getEntitySync(
279
+ if (className === 'User' || className === 'Group') {
280
+ throw new AccessControlError("You can't delete users or groups by ID.");
281
+ }
282
+ const entity = await nymph.getEntity(
221
283
  { class: nymph.getEntityClass(className ?? 'Entity') },
222
- { type: '&', guid: guid }
284
+ { type: '&', guid: guid },
223
285
  );
224
286
  if (entity != null) {
225
287
  checkPermissionsDelete(nymph, entity);
@@ -231,9 +293,9 @@ export default class Tilmeld implements TilmeldInterface {
231
293
  // disallowed changes to AC properties.
232
294
  const checkPermissionsSaveAndFilterAcChanges = async function (
233
295
  nymph: Nymph,
234
- entity: EntityInterface & AccessControlData
296
+ entity: EntityInterface & AccessControlData,
235
297
  ) {
236
- const tilmeld = nymph.tilmeld as Tilmeld;
298
+ const tilmeld = enforceTilmeld(nymph);
237
299
  if (!entity) {
238
300
  return;
239
301
  }
@@ -249,35 +311,9 @@ export default class Tilmeld implements TilmeldInterface {
249
311
  // allowing a change to ac properties.
250
312
 
251
313
  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
- };
314
+ const newAc = entity.$getCurrentAcValues();
273
315
 
274
- // Restore original AC properties and check permissions.
275
- setAcProperties(originalAc);
276
-
277
- function areAcPropertiesChanged(
278
- a: AccessControlData,
279
- b: AccessControlData
280
- ) {
316
+ function areAcPropertiesChanged(a: ACProperties, b: ACProperties) {
281
317
  for (const name of ['user', 'group'] as ('user' | 'group')[]) {
282
318
  const aVal = a[name];
283
319
  const bVal = b[name];
@@ -286,14 +322,12 @@ export default class Tilmeld implements TilmeldInterface {
286
322
  } else if (aVal == null || bVal == null) {
287
323
  return true;
288
324
  }
289
- if (!aVal.$is(bVal)) {
325
+ if (aVal !== bVal) {
290
326
  return true;
291
327
  }
292
328
  }
293
329
  for (const name of ['acUser', 'acGroup', 'acOther'] as (
294
- | 'acUser'
295
- | 'acGroup'
296
- | 'acOther'
330
+ 'acUser' | 'acGroup' | 'acOther'
297
331
  )[]) {
298
332
  const aVal = a[name];
299
333
  const bVal = b[name];
@@ -307,9 +341,7 @@ export default class Tilmeld implements TilmeldInterface {
307
341
  }
308
342
  }
309
343
  for (const name of ['acRead', 'acWrite', 'acFull'] as (
310
- | 'acRead'
311
- | 'acWrite'
312
- | 'acFull'
344
+ 'acRead' | 'acWrite' | 'acFull'
313
345
  )[]) {
314
346
  const aVal = a[name];
315
347
  const bVal = b[name];
@@ -321,12 +353,7 @@ export default class Tilmeld implements TilmeldInterface {
321
353
  if (aVal.length !== bVal.length) {
322
354
  return true;
323
355
  }
324
- if (
325
- xor(
326
- aVal.map((entity) => entity.guid),
327
- bVal.map((entity) => entity.guid)
328
- ).length
329
- ) {
356
+ if (xor(aVal, bVal).length) {
330
357
  return true;
331
358
  }
332
359
  }
@@ -335,41 +362,42 @@ export default class Tilmeld implements TilmeldInterface {
335
362
 
336
363
  if (areAcPropertiesChanged(originalAc, newAc)) {
337
364
  if (
338
- !tilmeld.checkPermissions(entity, TilmeldAccessLevels.FULL_ACCESS)
365
+ !tilmeld.checkPermissions(
366
+ entity,
367
+ TilmeldAccessLevels.FULL_ACCESS,
368
+ undefined,
369
+ originalAc,
370
+ )
339
371
  ) {
340
372
  // Only allow changes to AC properties if the user has full access.
341
373
  throw new AccessControlError(
342
- 'No permission to change access control properties.'
374
+ 'No permission to change access control properties.',
343
375
  );
344
376
  }
345
377
 
346
378
  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')
379
+ (originalAc.user != null && originalAc.user !== newAc.user) ||
380
+ (originalAc.user == null &&
381
+ newAc.user != null &&
382
+ newAc.user !== tilmeld.currentUser?.guid)
352
383
  ) {
353
384
  throw new AccessControlError(
354
- 'No permission to assign to another user.'
385
+ 'No permission to assign to another user.',
355
386
  );
356
387
  }
357
388
 
358
389
  if (
359
- newAc.group &&
360
- !newAc.group.$is(originalAc.group) &&
390
+ newAc.group != null &&
391
+ newAc.group !== originalAc.group &&
361
392
  !(
362
- newAc.group.$is(tilmeld.currentUser?.group) ||
363
- newAc.group.$inArray(tilmeld.currentUser?.groups ?? [])
364
- ) &&
365
- !tilmeld.gatekeeper('tilmeld/admin')
393
+ newAc.group === tilmeld.currentUser?.$getGid() ||
394
+ (tilmeld.currentUser?.$getGids() ?? []).includes(newAc.group)
395
+ )
366
396
  ) {
367
397
  throw new AccessControlError(
368
- 'No permission to assign to another group.'
398
+ 'No permission to assign to another group.',
369
399
  );
370
400
  }
371
-
372
- setAcProperties(newAc);
373
401
  }
374
402
  }
375
403
 
@@ -398,9 +426,9 @@ export default class Tilmeld implements TilmeldInterface {
398
426
  */
399
427
  const addAccess = async function (
400
428
  nymph: Nymph,
401
- entity: EntityInterface & AccessControlData
429
+ entity: EntityInterface & AccessControlData,
402
430
  ) {
403
- const tilmeld = nymph.tilmeld as Tilmeld;
431
+ const tilmeld = enforceTilmeld(nymph);
404
432
  const user = tilmeld.currentUser;
405
433
  if (
406
434
  user != null &&
@@ -411,12 +439,11 @@ export default class Tilmeld implements TilmeldInterface {
411
439
  if (entity.user == null) {
412
440
  entity.user = user;
413
441
  }
414
- if (
415
- entity.group == null &&
416
- user.group != null &&
417
- user.group.guid != null
418
- ) {
419
- entity.group = user.group;
442
+ if (entity.group == null) {
443
+ const group = user.group;
444
+ if (group != null && group.guid != null) {
445
+ entity.group = group;
446
+ }
420
447
  }
421
448
  if (entity.acUser == null) {
422
449
  entity.acUser = TilmeldAccessLevels.FULL_ACCESS;
@@ -442,7 +469,7 @@ export default class Tilmeld implements TilmeldInterface {
442
469
 
443
470
  const validate = async function (
444
471
  _nymph: Nymph,
445
- entity: EntityInterface & AccessControlData
472
+ entity: EntityInterface & AccessControlData,
446
473
  ) {
447
474
  if (!(entity instanceof User) && !(entity instanceof Group)) {
448
475
  const ownershipAcPropertyValidator = (prop: any) => {
@@ -452,7 +479,7 @@ export default class Tilmeld implements TilmeldInterface {
452
479
  prop > TilmeldAccessLevels.FULL_ACCESS
453
480
  ) {
454
481
  throw new AccessControlError(
455
- 'Invalid access control property: ' + prop
482
+ 'Invalid access control property: ' + prop,
456
483
  );
457
484
  }
458
485
  };
@@ -460,13 +487,13 @@ export default class Tilmeld implements TilmeldInterface {
460
487
  const accessAcPropertyValidator = (prop: any) => {
461
488
  if (!Array.isArray(prop)) {
462
489
  throw new AccessControlError(
463
- 'Invalid access control property: ' + prop
490
+ 'Invalid access control property: ' + prop,
464
491
  );
465
492
  }
466
493
  prop.forEach((value) => {
467
- if (!(value instanceof User || value instanceof Group)) {
494
+ if (typeof value !== 'string' || !value.match(/^[0-9a-f]{24}$/i)) {
468
495
  throw new AccessControlError(
469
- 'Invalid access control property: ' + prop
496
+ 'Invalid access control property: ' + prop,
470
497
  );
471
498
  }
472
499
  });
@@ -475,7 +502,7 @@ export default class Tilmeld implements TilmeldInterface {
475
502
  if ('user' in entity) {
476
503
  if (!(entity.user instanceof User)) {
477
504
  throw new AccessControlError(
478
- 'Invalid access control property: user'
505
+ 'Invalid access control property: user',
479
506
  );
480
507
  }
481
508
  }
@@ -483,7 +510,7 @@ export default class Tilmeld implements TilmeldInterface {
483
510
  if ('group' in entity) {
484
511
  if (!(entity.group instanceof Group)) {
485
512
  throw new AccessControlError(
486
- 'Invalid access control property: group'
513
+ 'Invalid access control property: group',
487
514
  );
488
515
  }
489
516
  }
@@ -528,18 +555,10 @@ export default class Tilmeld implements TilmeldInterface {
528
555
  */
529
556
  public addAccessControlSelectors(
530
557
  options: Options,
531
- selectors: FormattedSelector[]
558
+ selectors: FormattedSelector[],
532
559
  ) {
533
560
  const user = this.currentUser;
534
561
 
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
562
  if (options == null) {
544
563
  throw new Error('No options in argument.');
545
564
  } else if (
@@ -552,59 +571,112 @@ export default class Tilmeld implements TilmeldInterface {
552
571
  }
553
572
 
554
573
  if (user == null || user.guid == null) {
555
- selectors.push({
556
- type: '|',
574
+ // Access requests for only public entities.
575
+
576
+ const allAccessRequests =
577
+ TilmeldAccessRequest.OTHER_ACCESSIBLE | TilmeldAccessRequest.UNOWNED;
578
+ const requestedAccess = (options.acRequest ?? 0) & allAccessRequests;
579
+ const defaultAccess = requestedAccess === TilmeldAccessRequest.ALL_LEVELS;
580
+
581
+ const selector: FormattedSelector = { type: '|' };
582
+
583
+ if (
584
+ defaultAccess ||
585
+ requestedAccess & TilmeldAccessRequest.OTHER_ACCESSIBLE
586
+ ) {
557
587
  // Other access control is sufficient.
558
- gte: [['acOther', TilmeldAccessLevels.READ_ACCESS]],
588
+ selector.gte = [['acOther', TilmeldAccessLevels.READ_ACCESS]];
589
+ }
590
+
591
+ if (defaultAccess || requestedAccess & TilmeldAccessRequest.UNOWNED) {
559
592
  // The user and group are not set.
560
- selector: [
593
+ selector.selector = [
561
594
  {
562
595
  type: '&',
563
596
  '!defined': [['user', 'group']],
564
597
  },
565
- ],
566
- });
598
+ ];
599
+ }
600
+
601
+ if (Object.keys(selector).length <= 1) {
602
+ this.nymph.config.debugError(
603
+ 'tilmeld',
604
+ 'No access controls were added to the query. This should not be possible.',
605
+ );
606
+ throw new Error('Access control error.');
607
+ }
608
+
609
+ selectors.push(selector);
567
610
  } else {
568
- const subSelectors: FormattedSelector[] = [
569
- {
611
+ // Access requests for public and private entities.
612
+
613
+ const allAccessRequests =
614
+ TilmeldAccessRequest.USER_OWNED |
615
+ TilmeldAccessRequest.PRIMARY_GROUP_OWNED |
616
+ TilmeldAccessRequest.SECONDARY_GROUP_OWNED |
617
+ TilmeldAccessRequest.USER_ACCESSIBLE |
618
+ TilmeldAccessRequest.PRIMARY_GROUP_ACCESSIBLE |
619
+ TilmeldAccessRequest.SECONDARY_GROUP_ACCESSIBLE |
620
+ TilmeldAccessRequest.OTHER_ACCESSIBLE |
621
+ TilmeldAccessRequest.UNOWNED;
622
+ const requestedAccess = (options.acRequest ?? 0) & allAccessRequests;
623
+ const defaultAccess = requestedAccess === TilmeldAccessRequest.ALL_LEVELS;
624
+
625
+ const subSelectors: FormattedSelector[] = [];
626
+ if (defaultAccess || requestedAccess & TilmeldAccessRequest.UNOWNED) {
627
+ subSelectors.push({
570
628
  type: '&',
629
+ // The user and group are not set.
571
630
  '!defined': [['user', 'group']],
572
- },
631
+ });
632
+ }
633
+ // USER_OWNED is always enforced.
634
+ subSelectors.push(
573
635
  // It is owned by the user.
574
636
  {
575
637
  type: '&',
576
638
  ref: [['user', user.guid]],
577
639
  gte: [['acUser', TilmeldAccessLevels.READ_ACCESS]],
578
640
  },
579
- ];
641
+ );
580
642
  const groupRefs: FormattedSelector['ref'] = [];
581
- const acRefs: FormattedSelector['ref'] = [];
582
- if (user.group != null && user.group.guid != null) {
583
- // It belongs to the user's primary group.
584
- groupRefs.push(['group', user.group.guid]);
585
- // 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]);
643
+ const acContains: FormattedSelector['contain'] = [];
644
+ const gid = user.$getGid();
645
+ if (gid != null) {
646
+ if (
647
+ defaultAccess ||
648
+ requestedAccess & TilmeldAccessRequest.PRIMARY_GROUP_OWNED
649
+ ) {
650
+ // It belongs to the user's primary group.
651
+ groupRefs.push(['group', gid]);
652
+ }
653
+ if (
654
+ defaultAccess ||
655
+ requestedAccess & TilmeldAccessRequest.PRIMARY_GROUP_ACCESSIBLE
656
+ ) {
657
+ // User's primary group guid is listed in acRead, acWrite, or acFull.
658
+ acContains.push(['acRead', gid]);
659
+ acContains.push(['acWrite', gid]);
660
+ acContains.push(['acFull', gid]);
598
661
  }
599
662
  }
600
- for (let curDescendantGroup of user.$getDescendantGroupsSync()) {
601
- if (curDescendantGroup != null && curDescendantGroup.guid != null) {
663
+ const gids = user.$getGids();
664
+ for (let curGid of gids ?? []) {
665
+ if (
666
+ defaultAccess ||
667
+ requestedAccess & TilmeldAccessRequest.SECONDARY_GROUP_OWNED
668
+ ) {
602
669
  // 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]);
670
+ groupRefs.push(['group', curGid]);
671
+ }
672
+ if (
673
+ defaultAccess ||
674
+ requestedAccess & TilmeldAccessRequest.SECONDARY_GROUP_ACCESSIBLE
675
+ ) {
676
+ // User's secondary group guid is listed in acRead, acWrite, or acFull.
677
+ acContains.push(['acRead', curGid]);
678
+ acContains.push(['acWrite', curGid]);
679
+ acContains.push(['acFull', curGid]);
608
680
  }
609
681
  }
610
682
  // All the group refs.
@@ -620,26 +692,43 @@ export default class Tilmeld implements TilmeldInterface {
620
692
  ],
621
693
  });
622
694
  }
623
- // All the acRead, acWrite, and acFull refs.
624
- if (acRefs.length) {
695
+ // All the acRead, acWrite, and acFull contains.
696
+ if (acContains.length) {
625
697
  subSelectors.push({
626
698
  type: '|',
627
- ref: acRefs,
699
+ contain: acContains,
628
700
  });
629
701
  }
630
- const selector: FormattedSelector = {
631
- type: '|',
702
+ const selector: FormattedSelector = { type: '|' };
703
+ if (
704
+ defaultAccess ||
705
+ requestedAccess & TilmeldAccessRequest.OTHER_ACCESSIBLE
706
+ ) {
632
707
  // Other access control is sufficient.
633
- gte: [['acOther', TilmeldAccessLevels.READ_ACCESS]],
634
- // The user and group are not set.
635
- selector: subSelectors,
708
+ selector.gte = [['acOther', TilmeldAccessLevels.READ_ACCESS]];
709
+ }
710
+ if (subSelectors.length) {
711
+ // One of the sub selectors.
712
+ selector.selector = subSelectors;
713
+ }
714
+ if (
715
+ defaultAccess ||
716
+ requestedAccess & TilmeldAccessRequest.USER_ACCESSIBLE
717
+ ) {
636
718
  // The user is listed in acRead, acWrite, or acFull.
637
- ref: [
719
+ selector.contain = [
638
720
  ['acRead', user.guid],
639
721
  ['acWrite', user.guid],
640
722
  ['acFull', user.guid],
641
- ],
642
- };
723
+ ];
724
+ }
725
+ if (Object.keys(selector).length <= 1) {
726
+ this.nymph.config.debugError(
727
+ 'tilmeld',
728
+ 'No access controls were added to the query. This should not be possible.',
729
+ );
730
+ throw new Error('Access control error.');
731
+ }
643
732
  selectors.push(selector);
644
733
  }
645
734
  }
@@ -671,7 +760,7 @@ export default class Tilmeld implements TilmeldInterface {
671
760
  * - FULL_ACCESS, the user has read, write, and delete access to the entity,
672
761
  * as well as being able to manage its access controls and ownership.
673
762
  *
674
- * These properties defaults to:
763
+ * These properties default to:
675
764
  *
676
765
  * - acUser = TilmeldAccessLevels.FULL_ACCESS
677
766
  * - acGroup = TilmeldAccessLevels.READ_ACCESS
@@ -686,9 +775,9 @@ export default class Tilmeld implements TilmeldInterface {
686
775
  * The following conditions will result in different checks, which determine
687
776
  * whether the check passes:
688
777
  *
689
- * - The user has the "system/admin" ability. (Always true.)
690
778
  * - It is a user or group. (True for READ_ACCESS or Tilmeld admins.)
691
- * - The entity has no "user" and no "group". (Always true.)
779
+ * - It is a user or group that belongs to a domain the current user is an
780
+ * admin for, and domain support is enabled. (Always true.)
692
781
  * - No user is logged in. (Check other AC.)
693
782
  * - The entity is the user. (Always true.)
694
783
  * - It is the user's primary group. (True for READ_ACCESS.)
@@ -706,16 +795,17 @@ export default class Tilmeld implements TilmeldInterface {
706
795
  * @param entity The entity to check.
707
796
  * @param type The lowest level of permission to consider a pass.
708
797
  * @param user The user to check permissions for. If null, uses the current user. If false, checks for public access.
798
+ * @param acProperties The acProperties to use instead of getting them from the entity.
709
799
  * @returns Whether the current user has at least `type` permission for the entity.
710
800
  */
711
801
  public checkPermissions(
712
- entity: EntityInterface & AccessControlData,
802
+ entity: EntityInterface,
713
803
  type: TilmeldAccessLevels = TilmeldAccessLevels.READ_ACCESS,
714
- user?: (User & UserData) | false
804
+ user?: (User & UserData) | false,
805
+ acProperties?: ACProperties,
715
806
  ) {
716
- // Only works for entities.
717
- if (typeof entity !== 'object' || typeof entity.$is !== 'function') {
718
- return false;
807
+ if (!acProperties) {
808
+ acProperties = entity.$getCurrentAcValues() as ACProperties;
719
809
  }
720
810
 
721
811
  let userOrNull: (User & UserData) | null = null;
@@ -729,30 +819,43 @@ export default class Tilmeld implements TilmeldInterface {
729
819
  userOrEmpty = user;
730
820
  }
731
821
 
732
- if (userOrEmpty.$gatekeeper('system/admin')) {
733
- return true;
734
- }
735
-
736
822
  // Users and groups are always readable. Editable by Tilmeld admins.
737
823
  if (
738
824
  (entity instanceof User || entity instanceof Group) &&
739
825
  (type === TilmeldAccessLevels.READ_ACCESS ||
740
- userOrEmpty.$gatekeeper('tilmeld/admin'))
826
+ userOrEmpty.abilities?.includes('tilmeld/admin') ||
827
+ userOrEmpty.abilities?.includes('system/admin'))
741
828
  ) {
742
829
  return true;
743
830
  }
744
831
 
745
- // Entities with no owners are always editable.
746
- if (entity.user == null && entity.group == null) {
747
- return true;
832
+ // Users and groups that belong to a domain are readable and editable by
833
+ // that domain's admins.
834
+ if (
835
+ this.config.domainSupport &&
836
+ (entity instanceof User || entity instanceof Group)
837
+ ) {
838
+ const [_local, domain] = splitn(
839
+ (entity instanceof User
840
+ ? (entity as User & UserData).username
841
+ : (entity as Group & GroupData).groupname) ?? '',
842
+ '@',
843
+ 2,
844
+ );
845
+ if (
846
+ domain &&
847
+ userOrEmpty.abilities?.includes(`tilmeld/domain/${domain}/admin`)
848
+ ) {
849
+ return true;
850
+ }
748
851
  }
749
852
 
750
853
  // 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;
854
+ const acUser = acProperties.acUser ?? TilmeldAccessLevels.FULL_ACCESS;
855
+ const acGroup = acProperties.acGroup ?? TilmeldAccessLevels.READ_ACCESS;
856
+ const acOther = acProperties.acOther ?? TilmeldAccessLevels.NO_ACCESS;
754
857
 
755
- if (userOrNull === null) {
858
+ if (userOrNull == null) {
756
859
  return acOther >= type;
757
860
  }
758
861
 
@@ -762,45 +865,59 @@ export default class Tilmeld implements TilmeldInterface {
762
865
  }
763
866
 
764
867
  // Check if the entity is the user's group. Always readable.
868
+ const gid = userOrEmpty.$getGid();
869
+ if (
870
+ gid != null &&
871
+ entity.guid === gid &&
872
+ type === TilmeldAccessLevels.READ_ACCESS
873
+ ) {
874
+ return true;
875
+ }
876
+
877
+ // Check if the entity is one of the user's secondary groups. Always
878
+ // readable.
879
+ const gids = userOrEmpty.$getGids();
765
880
  if (
766
- userOrEmpty.group != null &&
767
- typeof userOrEmpty.group.$is === 'function' &&
768
- userOrEmpty.group.$is(entity) &&
881
+ gids != null &&
882
+ entity.guid != null &&
883
+ gids.indexOf(entity.guid) !== -1 &&
769
884
  type === TilmeldAccessLevels.READ_ACCESS
770
885
  ) {
771
886
  return true;
772
887
  }
773
888
 
889
+ // At this point, if the entity is a user or group, it should not be
890
+ // accessible.
891
+ if (entity instanceof User || entity instanceof Group) {
892
+ return false;
893
+ }
894
+
774
895
  // 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());
896
+ let allGids = gid == null ? [] : [gid];
897
+ allGids = allGids.concat(gids ?? []);
778
898
 
779
899
  // Check access ac properties.
780
900
  const checks = [
781
901
  {
782
902
  type: TilmeldAccessLevels.FULL_ACCESS,
783
- array: [...(entity.acFull ?? [])],
903
+ array: [...(acProperties.acFull ?? [])],
784
904
  },
785
905
  {
786
906
  type: TilmeldAccessLevels.WRITE_ACCESS,
787
- array: [...(entity.acWrite ?? [])],
907
+ array: [...(acProperties.acWrite ?? [])],
788
908
  },
789
909
  {
790
910
  type: TilmeldAccessLevels.READ_ACCESS,
791
- array: [...(entity.acRead ?? [])],
911
+ array: [...(acProperties.acRead ?? [])],
792
912
  },
793
913
  ];
794
914
  for (let curCheck of checks) {
795
915
  if (type <= curCheck.type) {
796
- if (userOrEmpty.$inArray(curCheck.array)) {
916
+ if (curCheck.array.includes(userOrEmpty.guid)) {
797
917
  return true;
798
918
  }
799
- for (let curGroup of allGroups) {
800
- if (
801
- typeof curGroup.$inArray === 'function' &&
802
- curGroup.$inArray(curCheck.array)
803
- ) {
919
+ for (let curGid of allGids) {
920
+ if (curCheck.array.includes(curGid)) {
804
921
  return true;
805
922
  }
806
923
  }
@@ -808,18 +925,10 @@ export default class Tilmeld implements TilmeldInterface {
808
925
  }
809
926
 
810
927
  // Check ownership ac properties.
811
- if (
812
- entity.user != null &&
813
- typeof entity.user.$is === 'function' &&
814
- entity.user.$is(userOrNull)
815
- ) {
928
+ if (acProperties.user != null && acProperties.user === userOrNull.guid) {
816
929
  return acUser >= type;
817
930
  }
818
- if (
819
- entity.group != null &&
820
- typeof entity.group.$inArray === 'function' &&
821
- entity.group.$inArray(allGroups)
822
- ) {
931
+ if (acProperties.group != null && allGids.includes(acProperties.group)) {
823
932
  return acGroup >= type;
824
933
  }
825
934
  return acOther >= type;
@@ -849,10 +958,10 @@ export default class Tilmeld implements TilmeldInterface {
849
958
  * @param user The user to check permissions for. If null, uses the current user. If false, checks for public access.
850
959
  * @returns Whether the current user has at least `type` permission for the UID.
851
960
  */
852
- public checkClientUIDPermissions(
961
+ public async checkClientUIDPermissions(
853
962
  name: string,
854
963
  type: TilmeldAccessLevels = TilmeldAccessLevels.READ_ACCESS,
855
- user?: (User & UserData) | false
964
+ user?: (User & UserData) | false,
856
965
  ) {
857
966
  let userOrEmpty: User & UserData = this.User.factorySync();
858
967
  // Calculate the user.
@@ -862,30 +971,30 @@ export default class Tilmeld implements TilmeldInterface {
862
971
  userOrEmpty = user;
863
972
  }
864
973
 
865
- if (userOrEmpty.$gatekeeper('system/admin')) {
974
+ if (userOrEmpty.abilities?.includes('system/admin')) {
866
975
  return true;
867
976
  }
868
977
 
869
978
  if (type === TilmeldAccessLevels.FULL_ACCESS) {
870
979
  return (
871
- this.config.clientSetabledUIDs.indexOf(name) !== -1 ||
872
- userOrEmpty.$gatekeeper(`uid/set/${name}`)
980
+ this.config.clientSetabledUIDs.includes(name) ||
981
+ (await userOrEmpty.$gatekeeper(`uid/set/${name}`))
873
982
  );
874
983
  } else if (type === TilmeldAccessLevels.WRITE_ACCESS) {
875
984
  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}`)
985
+ this.config.clientEnabledUIDs.includes(name) ||
986
+ this.config.clientSetabledUIDs.includes(name) ||
987
+ (await userOrEmpty.$gatekeeper(`uid/new/${name}`)) ||
988
+ (await userOrEmpty.$gatekeeper(`uid/set/${name}`))
880
989
  );
881
990
  } else if (type === TilmeldAccessLevels.READ_ACCESS) {
882
991
  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}`)
992
+ this.config.clientReadableUIDs.includes(name) ||
993
+ this.config.clientEnabledUIDs.includes(name) ||
994
+ this.config.clientSetabledUIDs.includes(name) ||
995
+ (await userOrEmpty.$gatekeeper(`uid/get/${name}`)) ||
996
+ (await userOrEmpty.$gatekeeper(`uid/new/${name}`)) ||
997
+ (await userOrEmpty.$gatekeeper(`uid/set/${name}`))
889
998
  );
890
999
  }
891
1000
 
@@ -897,20 +1006,13 @@ export default class Tilmeld implements TilmeldInterface {
897
1006
  *
898
1007
  * @param user The user.
899
1008
  */
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;
1009
+ public async fillSession(user: User & UserData) {
905
1010
  this.currentUser = user;
1011
+ this.gatekeeperCache = await user.$getGatekeeperCache();
1012
+
906
1013
  // Now update the data protection on the user and all the groups.
907
1014
  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
- }
1015
+ this.currentUser.$updateGroupDataProtection();
914
1016
  }
915
1017
 
916
1018
  /**
@@ -919,14 +1021,10 @@ export default class Tilmeld implements TilmeldInterface {
919
1021
  public clearSession() {
920
1022
  const user = this.currentUser;
921
1023
  this.currentUser = null;
1024
+ this.gatekeeperCache = null;
922
1025
  if (user) {
923
1026
  user.$updateDataProtection();
924
- if (user.group != null) {
925
- user.group.$updateDataProtection();
926
- }
927
- for (let group of user.groups ?? []) {
928
- group.$updateDataProtection();
929
- }
1027
+ user.$updateGroupDataProtection();
930
1028
  }
931
1029
  }
932
1030
 
@@ -937,6 +1035,7 @@ export default class Tilmeld implements TilmeldInterface {
937
1035
  * @returns The user on success, null on failure.
938
1036
  */
939
1037
  public async extractToken(token: string) {
1038
+ this.nymph.config.debugInfo('tilmeld', 'Extracting and validating token.');
940
1039
  const extract = this.config.jwtExtract(this.config, token);
941
1040
  if (extract == null) {
942
1041
  return null;
@@ -948,9 +1047,9 @@ export default class Tilmeld implements TilmeldInterface {
948
1047
  {
949
1048
  type: '&',
950
1049
  guid: guid,
951
- }
1050
+ },
952
1051
  );
953
- if (!user || !user.guid || !user.enabled) {
1052
+ if (!user || !user.guid) {
954
1053
  return null;
955
1054
  }
956
1055
 
@@ -958,40 +1057,73 @@ export default class Tilmeld implements TilmeldInterface {
958
1057
  }
959
1058
 
960
1059
  /**
961
- * Check for a TILMELDAUTH token, and, if set, authenticate from it.
1060
+ * Check for TILMELDAUTH and TILMELDSWITCH tokens, and, if set, authenticate
1061
+ * from it/them.
1062
+ *
1063
+ * You can also call this function after setting `response.locals.user` to the
1064
+ * user you want to authenticate. You *should* check for `user.enabled` before
1065
+ * setting this variable, unless you explicitly want to log in as a disabled
1066
+ * user. (The user must be an instance of the User class for this Tilmeld
1067
+ * instance.)
1068
+ *
1069
+ * This function will set `response.locals.user` to the logged in user on
1070
+ * successful authentication.
962
1071
  *
963
1072
  * @param skipXsrfToken Skip the XSRF token check.
1073
+ * @param skipRenew Skip the token renewal step, even if the token is close to expiration.
964
1074
  * @returns True if a user was authenticated, false on any failure.
965
1075
  */
966
- public authenticate(skipXsrfToken = false) {
1076
+ public async authenticate(skipXsrfToken = false, skipRenew = false) {
967
1077
  if (this.request == null) {
968
1078
  return false;
969
1079
  }
970
1080
 
1081
+ if (
1082
+ this.response &&
1083
+ this.response.locals.user &&
1084
+ this.response.locals.user instanceof this.User
1085
+ ) {
1086
+ // The user has already been authenticated through some other means.
1087
+ const user = this.response.locals.user;
1088
+ await this.fillSession(user);
1089
+ return true;
1090
+ }
1091
+
971
1092
  const cookies = this.request.cookies ?? {};
972
1093
 
973
1094
  // If a client does't support cookies, they can use the X-TILMELDAUTH header
974
1095
  // to provide the auth token.
975
1096
  let fromAuthHeader = false;
976
1097
  let authToken: string;
1098
+ let switchToken: string | null;
977
1099
  if (this.request.header('x-tilmeldauth') != null) {
978
1100
  fromAuthHeader = true;
979
1101
  authToken = this.request.header('x-tilmeldauth') as string;
1102
+ switchToken =
1103
+ (this.request.header('x-tilmeldswitch') as string) ||
1104
+ 'TILMELDSWITCH' in cookies
1105
+ ? cookies.TILMELDSWITCH
1106
+ : null;
980
1107
  } else if ('TILMELDAUTH' in cookies) {
981
1108
  fromAuthHeader = false;
982
1109
  authToken = cookies.TILMELDAUTH;
1110
+ switchToken = 'TILMELDSWITCH' in cookies ? cookies.TILMELDSWITCH : null;
983
1111
  } else {
984
1112
  return false;
985
1113
  }
986
1114
 
987
- let extract: { guid: string; expire: Date } | null;
1115
+ let authExtract: { guid: string; issued: Date; expire: Date } | null;
1116
+ let switchExtract: { guid: string; expire: Date } | null;
988
1117
  if (
989
1118
  skipXsrfToken ||
990
1119
  this.request.originalUrl.startsWith(this.config.setupPath)
991
1120
  ) {
992
1121
  // The request is for the setup app, or we were told to skip the XSRF
993
1122
  // check, so don't check for the XSRF token.
994
- extract = this.config.jwtExtract(this.config, authToken);
1123
+ authExtract = this.config.jwtExtract(this.config, authToken);
1124
+ switchExtract = switchToken
1125
+ ? this.config.jwtExtract(this.config, switchToken)
1126
+ : null;
995
1127
  } else {
996
1128
  // The request is for something else, so check for a valid XSRF token,
997
1129
  // unless the auth token is provided by a header (instead of a cookie).
@@ -1000,28 +1132,65 @@ export default class Tilmeld implements TilmeldInterface {
1000
1132
  return false;
1001
1133
  }
1002
1134
 
1003
- extract = this.config.jwtExtract(this.config, authToken, xsrfToken);
1135
+ authExtract = this.config.jwtExtract(this.config, authToken, xsrfToken);
1136
+ switchExtract = switchToken
1137
+ ? this.config.jwtExtract(this.config, switchToken)
1138
+ : null;
1004
1139
  }
1005
1140
 
1006
- if (extract == null) {
1007
- this.logout();
1141
+ if (authExtract == null) {
1142
+ await this.logout();
1008
1143
  return false;
1009
1144
  }
1010
- const { guid, expire } = extract;
1145
+ const { guid, issued, expire } = authExtract;
1146
+ const { guid: switchGuid, expire: switchExpire } = switchExtract || {
1147
+ guid: null,
1148
+ expire: null,
1149
+ };
1011
1150
 
1012
- const user = this.User.factorySync(guid);
1013
- if (user.guid == null || !user.enabled) {
1014
- this.logout();
1151
+ const user = await this.User.factory(guid);
1152
+ if (
1153
+ user.guid == null ||
1154
+ !user.enabled ||
1155
+ expire.getTime() <= Date.now() ||
1156
+ issued.getTime() > Date.now() ||
1157
+ (user.revokeTokenDate != null && issued.getTime() <= user.revokeTokenDate)
1158
+ ) {
1159
+ await this.logout();
1015
1160
  return false;
1016
1161
  }
1017
1162
 
1018
- if (expire.valueOf() < Date.now() + this.config.jwtRenew * 1000) {
1163
+ if (switchGuid && switchExpire && switchExpire.getTime() > Date.now()) {
1164
+ // Load the switch user.
1165
+ const switchUser = await this.User.factory(switchGuid);
1166
+ if (switchUser.guid == null) {
1167
+ await this.logoutSwitch();
1168
+ } else {
1169
+ await this.fillSession(switchUser);
1170
+
1171
+ if (this.response) {
1172
+ this.response.locals.user = switchUser;
1173
+ }
1174
+
1175
+ return true;
1176
+ }
1177
+ }
1178
+
1179
+ if (
1180
+ !skipRenew &&
1181
+ expire.getTime() < Date.now() + this.config.jwtRenew * 1000
1182
+ ) {
1019
1183
  // If the user is less than renew time from needing a new token, give them
1020
1184
  // a new one.
1021
- this.login(user, fromAuthHeader);
1185
+ await this.login(user, fromAuthHeader);
1022
1186
  } else {
1023
- this.fillSession(user);
1187
+ await this.fillSession(user);
1024
1188
  }
1189
+
1190
+ if (this.response) {
1191
+ this.response.locals.user = user;
1192
+ }
1193
+
1025
1194
  return true;
1026
1195
  }
1027
1196
 
@@ -1029,27 +1198,87 @@ export default class Tilmeld implements TilmeldInterface {
1029
1198
  * Logs the given user into the system.
1030
1199
  *
1031
1200
  * @param user The user.
1032
- * @param sendAuthHeader When true, a custom header with the auth token will be sent.
1201
+ * @param sendAuthHeader Send the auth token as a custom header.
1202
+ * @param sendCookie Send the auth token as a cookie.
1033
1203
  * @returns True on success, false on failure.
1034
1204
  */
1035
- public login(user: User & UserData, sendAuthHeader: boolean) {
1205
+ public async login(
1206
+ user: User & UserData,
1207
+ sendAuthHeader: boolean,
1208
+ sendCookie = true,
1209
+ ) {
1036
1210
  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);
1211
+ if (this.response) {
1212
+ if (!this.response.headersSent) {
1213
+ const token = this.config.jwtBuilder(this.config, user);
1214
+
1215
+ if (sendCookie) {
1216
+ const appUrl = new URL(this.config.appUrl);
1217
+ this.response.cookie('TILMELDAUTH', token, {
1218
+ domain: this.config.cookieDomain,
1219
+ path: this.config.cookiePath,
1220
+ maxAge: this.config.jwtExpire * 1000,
1221
+ secure: appUrl.protocol === 'https:',
1222
+ httpOnly: false, // Allow JS access (for CSRF protection).
1223
+ sameSite: appUrl.protocol === 'https:' ? 'lax' : 'strict',
1224
+ });
1225
+ }
1226
+
1227
+ if (sendAuthHeader) {
1228
+ this.response.set('X-TILMELDAUTH', token);
1229
+ }
1230
+ }
1231
+
1232
+ this.response.locals.user = user;
1233
+ }
1234
+
1235
+ await this.fillSession(user);
1236
+ return true;
1237
+ }
1238
+ return false;
1239
+ }
1240
+
1241
+ /**
1242
+ * Adds a switch auth token for the given user.
1243
+ *
1244
+ * This effectively logs the current user in to the system as the given user.
1245
+ *
1246
+ * @param user The user.
1247
+ * @param sendAuthHeader Send the auth token as a custom header.
1248
+ * @param sendCookie Send the auth token as a cookie.
1249
+ * @returns True on success, false on failure.
1250
+ */
1251
+ public async loginSwitch(
1252
+ user: User & UserData,
1253
+ sendAuthHeader: boolean,
1254
+ sendCookie = true,
1255
+ ) {
1256
+ if (user.guid != null) {
1257
+ if (this.response) {
1258
+ if (!this.response.headersSent) {
1259
+ const token = this.config.jwtBuilder(this.config, user, true);
1260
+
1261
+ if (sendCookie) {
1262
+ const appUrl = new URL(this.config.appUrl);
1263
+ this.response.cookie('TILMELDSWITCH', token, {
1264
+ domain: this.config.cookieDomain,
1265
+ path: this.config.cookiePath,
1266
+ maxAge: this.config.jwtSwitchExpire * 1000,
1267
+ secure: appUrl.protocol === 'https:',
1268
+ httpOnly: false, // Allow JS access (for PubSub auth token).
1269
+ sameSite: appUrl.protocol === 'https:' ? 'lax' : 'strict',
1270
+ });
1271
+ }
1272
+
1273
+ if (sendAuthHeader) {
1274
+ this.response.set('X-TILMELDSWITCH', token);
1275
+ }
1050
1276
  }
1277
+
1278
+ this.response.locals.user = user;
1051
1279
  }
1052
- this.fillSession(user);
1280
+
1281
+ await this.fillSession(user);
1053
1282
  return true;
1054
1283
  }
1055
1284
  return false;
@@ -1057,15 +1286,50 @@ export default class Tilmeld implements TilmeldInterface {
1057
1286
 
1058
1287
  /**
1059
1288
  * Logs the current user out of the system.
1289
+ *
1290
+ * @param clearCookie Clear the auth cookie. (Also send a header.)
1060
1291
  */
1061
- public logout() {
1292
+ public async logout(clearCookie = true) {
1293
+ if (this.request && !this.alreadyLoggedOutSwitch) {
1294
+ const cookies = this.request.cookies ?? {};
1295
+
1296
+ if ('TILMELDSWITCH' in cookies) {
1297
+ this.alreadyLoggedOutSwitch = true;
1298
+ return await this.logoutSwitch(clearCookie);
1299
+ }
1300
+ }
1301
+
1062
1302
  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', '');
1303
+ if (this.response) {
1304
+ if (clearCookie && !this.response.headersSent) {
1305
+ this.response.clearCookie('TILMELDAUTH', {
1306
+ domain: this.config.cookieDomain,
1307
+ path: this.config.cookiePath,
1308
+ });
1309
+ this.response.set('X-TILMELDAUTH', '');
1310
+ }
1311
+ this.response.locals.user = null;
1312
+ }
1313
+ }
1314
+
1315
+ /**
1316
+ * Clears the switch user out of the system.
1317
+ *
1318
+ * @param clearCookie Clear the auth cookie. (Also send a header.)
1319
+ */
1320
+ public async logoutSwitch(clearCookie = true) {
1321
+ this.clearSession();
1322
+ if (this.response) {
1323
+ if (clearCookie && !this.response.headersSent) {
1324
+ this.response.clearCookie('TILMELDSWITCH', {
1325
+ domain: this.config.cookieDomain,
1326
+ path: this.config.cookiePath,
1327
+ });
1328
+ this.response.set('X-TILMELDSWITCH', '');
1329
+ }
1330
+ this.response.locals.user = null;
1331
+
1332
+ await this.authenticate(false, true);
1069
1333
  }
1070
1334
  }
1071
1335